From a70c7a50ea48143e5d85a7dfec1b7c4bb99470de Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Thu, 26 Mar 2026 14:33:26 -0600 Subject: [PATCH 01/93] established missing implementation warning framework --- src/buildcompiler/buildcompiler.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index 241b67c..da562c4 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -1,5 +1,6 @@ -import random import sbol2 +import random +import warnings from typing import List, Dict from buildcompiler.plasmid import Plasmid @@ -165,16 +166,19 @@ def _remove_internal_bsai_sites(sequence: str) -> tuple[str, int]: None, ) if bsaI_impl is None: - raise ValueError( - "BsaI Restriction enzyme not found in provided collections. Terminating domestication." + self._create_RE_implementation("BsaI") + warnings.warn( + "BsaI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.", + RuntimeWarning, ) ligase_impl = ( self.ligase_implementations[0] if self.ligase_implementations else None ) if ligase_impl is None: - raise ValueError( - "No appropriate ligase found in provided collections. Terminating domestication." + self._create_ligase_implementation() + warnings.warn( + "No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol." ) dsDNAs = [] @@ -591,3 +595,9 @@ def _is_single_part(self, plasmid: sbol2.ComponentDefinition) -> bool: return True return False + + def _create_RE_implementation(name: str): + pass + + def _create_ligase_implementation(): + pass From 2653177479b66de3256bfe85fa0c9fd4d13c13a6 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 31 Mar 2026 14:01:20 -0600 Subject: [PATCH 02/93] incorrect draft of lvl2 --- notebooks/build_compiler_test.ipynb | 114 ++++++++++++++++++++++++++-- 1 file changed, 106 insertions(+), 8 deletions(-) diff --git a/notebooks/build_compiler_test.ipynb b/notebooks/build_compiler_test.ipynb index b9f1e14..d30433f 100644 --- a/notebooks/build_compiler_test.ipynb +++ b/notebooks/build_compiler_test.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 19, "id": "87bdb42e", "metadata": {}, "outputs": [], @@ -14,24 +14,32 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 20, "id": "e60a9c84", "metadata": {}, "outputs": [], "source": [ "design_doc = sbol2.Document()\n", - "design_doc.read(\"../tests/test_files/moclo_parts_circuit.xml\")\n", - "design = extract_toplevel_definition(design_doc)" + "design_doc.read(\"../tests/test_files/ExampleLvl2_design.xml\")" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 21, "id": "90648527", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Indexing collection: https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\n", + "Indexing collection: https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\n" + ] + } + ], "source": [ - "auth = \"51102d98-f852-4386-9ae8-7c5814d679c1\"\n", + "auth = \"b9a7ee3a-5a02-42cd-ad1a-454b68cbe2a1\"\n", "collections = [\n", " \"https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\",\n", " \"https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\",\n", @@ -39,6 +47,96 @@ "buildcompiler = BuildCompiler(collections, \"https://synbiohub.org\", auth, None)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "1712f5ce", + "metadata": {}, + "outputs": [], + "source": [ + "from buildcompiler.plasmid import Plasmid\n", + "from buildcompiler.abstract_translator import get_or_pull\n", + "from typing import List\n", + "from buildcompiler.constants import AMP\n", + "\n", + "\n", + "def _extract_lvl2_design_parts(\n", + " self, design_doc: sbol2.Document\n", + ") -> List[List[sbol2.ComponentDefinition]]:\n", + " \"\"\"\n", + " Returns definitions of level-0 parts grouped by level-1 components.\n", + "\n", + " Args:\n", + " design: :class:`sbol2.Document` containing the design.\n", + "\n", + " Returns:\n", + " A list where each element corresponds to a level-1 component\n", + " and contains a list of its part definitions in sequential order.\n", + " \"\"\"\n", + " result = []\n", + "\n", + " design = extract_toplevel_definition(design_doc)\n", + "\n", + " for lvl1_comp in design.getInSequentialOrder():\n", + " lvl0_ordered = self.sbol_doc.get(lvl1_comp.definition).getInSequentialOrder()\n", + "\n", + " parts = [\n", + " get_or_pull(self.sbol_doc, self.sbh, lvl0_comp.definition)\n", + " for lvl0_comp in lvl0_ordered\n", + " ]\n", + "\n", + " result.append(parts)\n", + "\n", + " return result\n", + "\n", + "\n", + "def assembly_lvl2(\n", + " self_buildcompiler, abstract_design_doc: sbol2.Document, backbone: Plasmid = None\n", + ") -> list[sbol2.ComponentDefinition]:\n", + " \"\"\"Assemble level-2 plasmids for the full design.\n", + "\n", + " Uses the assembled lvl1 plasmids and the current design to assemble\n", + " lvl2 plasmids in the correct order.\n", + "\n", + " :returns: List of assembled lvl2 plasmids.\n", + " :rtype: list[Plasmid]\n", + " :raises LookupError: If compatible plasmids or backbones cannot be found.\n", + " \"\"\"\n", + " # get high level genes, send to assembly_lvl1\n", + " # send original abstract_design to get a new dictionary\n", + " # send new dictionary to _get_backbone or get_compatible plasmids with AMP\n", + " TUs = _extract_lvl2_design_parts(self_buildcompiler, abstract_design_doc)\n", + "\n", + " for lvl1_comp_list in TUs:\n", + " plasmid_dict = self_buildcompiler._construct_plasmid_dict(lvl1_comp_list, AMP)\n", + " print(plasmid_dict)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "3e2272ff", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1\n", + "https://synbiohub.org/user/Gon/CIDARMoCloParts/B0032/1\n", + "https://synbiohub.org/user/Gon/CIDARMoCloParts/E0040m_gfp/1\n", + "https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1\n", + "https://synbiohub.org/user/Gon/CIDARMoCloParts/J23116/1\n", + "https://synbiohub.org/user/Gon/CIDARMoCloParts/B0033/1\n", + "https://synbiohub.org/user/Gon/CIDARMoCloParts/E1010m_rfp/1\n", + "https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1\n" + ] + } + ], + "source": [ + "assembly_lvl2(buildcompiler, design_doc)" + ] + }, { "cell_type": "code", "execution_count": null, @@ -72,7 +170,7 @@ "print(buildcompiler.restriction_enzyme_implementations)\n", "print(buildcompiler.ligase_implementations)\n", "\n", - "composite_plasmids = buildcompiler.assembly_lvl1(design, None)" + "# composite_plasmids = buildcompiler.assembly_lvl1(design, None)" ] }, { From 8f8c624435c1f93bbf01e0fb98e0902faa332121 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 31 Mar 2026 17:43:33 -0600 Subject: [PATCH 03/93] resolving original plasmid part overwrite issue in digestion --- src/buildcompiler/sbol2build.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/buildcompiler/sbol2build.py b/src/buildcompiler/sbol2build.py index fc731bf..7a19bdb 100644 --- a/src/buildcompiler/sbol2build.py +++ b/src/buildcompiler/sbol2build.py @@ -50,7 +50,9 @@ def __init__( # TODO add fields for activity/agent/plan self.ligase = ligase self.extracted_parts = [] # list of tuples [ComponentDefinition, Sequence] self.source_document = document - self.final_document = sbol2.Document() + self.final_document = ( + sbol2.Document() + ) # TODO change to allow this to be passed in as a parameter self.assembly_activity = initialize_assembly_activity() self.composites = [] @@ -497,8 +499,20 @@ def part_digestion( # find + add original component to product def & annotation for comp in reactant_component_definition.components: if comp.definition == original_part_def_URI: - prod_component_definition.components.add(comp) - part_extract_annotation.component = comp + new_comp = prod_component_definition.components.create(comp.displayId) + new_comp.definition = comp.definition + part_extract_annotation.component = new_comp + + original_cd = document.getComponentDefinition(comp.definition) + seq = document.get(original_cd.sequences[0]) + + new_seq = sbol2.Sequence( + uri=f"{reactant_component_definition.displayId}_extracted_part_seq", + elements=seq.elements, + encoding=seq.encoding, + ) + prod_component_definition.sequences.append(new_seq) + extracts_list.append((new_comp, new_seq)) prod_component_definition.sequenceAnnotations.add(three_prime_overhang_annotation) prod_component_definition.sequenceAnnotations.add(five_prime_overhang_annotation) @@ -1000,6 +1014,9 @@ def ligation( composite_implementation.built = composite_component_definition.identity composite_implementation.wasGeneratedBy = assembly_activity.identity + source_document.add_list( + [composite_component_definition, composite_seq, composite_implementation] + ) final_document.add_list( [composite_component_definition, composite_seq, composite_implementation] ) From 6083612393e8d83dc1f0acd16d99864593ab6ccc Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 31 Mar 2026 19:41:50 -0600 Subject: [PATCH 04/93] unique objects for each lvl1 + passing in final doc for continuity --- src/buildcompiler/buildcompiler.py | 16 +++++-- src/buildcompiler/sbol2build.py | 68 +++++++++++++++--------------- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index da562c4..01f49f4 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -278,7 +278,11 @@ def _remove_internal_bsai_sites(sequence: str) -> tuple[str, int]: return domesticated_parts def assembly_lvl1( - self, abstract_design: sbol2.ComponentDefinition, backbone: Plasmid = None + self, + abstract_design: sbol2.ComponentDefinition, + final_doc: sbol2.Document = sbol2.Document(), + product_name: str = None, + backbone: Plasmid = None, ) -> list[sbol2.ComponentDefinition]: """Assemble level-1 plasmids for each gene/transcriptional unit. @@ -322,9 +326,15 @@ def assembly_lvl1( ) assembly = Assembly( - compatible_plasmids, backbone, bsaI_impl, ligase_impl, self.sbol_doc + compatible_plasmids, + backbone, + bsaI_impl, + ligase_impl, + self.sbol_doc, + final_doc, + product_name, ) - composite_plasmids, product_doc = assembly.run() + composite_plasmids, product_doc = assembly.run() # TODO upload product_doc? self.indexed_plasmids.extend(composite_plasmids) diff --git a/src/buildcompiler/sbol2build.py b/src/buildcompiler/sbol2build.py index 7a19bdb..4298c18 100644 --- a/src/buildcompiler/sbol2build.py +++ b/src/buildcompiler/sbol2build.py @@ -42,22 +42,23 @@ def __init__( # TODO add fields for activity/agent/plan backbone_plasmid: Plasmid, restriction_enzyme: sbol2.Implementation, # TODO search for implementation in document, or domesticate the RE ligase: sbol2.Implementation, - document: sbol2.Document, + source_document: sbol2.Document, + final_document: sbol2.Document, + composite_prefix: str = "composite", ): self.part_plasmids = part_plasmids self.backbone = backbone_plasmid self.restriction_enzyme = restriction_enzyme self.ligase = ligase self.extracted_parts = [] # list of tuples [ComponentDefinition, Sequence] - self.source_document = document - self.final_document = ( - sbol2.Document() - ) # TODO change to allow this to be passed in as a parameter - self.assembly_activity = initialize_assembly_activity() + self.source_document = source_document + self.final_document = final_document + self.composite_prefix = composite_prefix + self.assembly_activity = self.initialize_assembly_activity() self.composites = [] def run( - self, include_extracted_parts=False + self, include_extracted_parts: bool = False ) -> List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]]: """Runs full assembly simulation. @@ -98,6 +99,7 @@ def run( self.composites = ligation( self.extracted_parts, self.assembly_activity, + self.composite_prefix, self.source_document, self.final_document, self.ligase, @@ -118,6 +120,27 @@ def run( return composite_plasmid_objs, self.final_document + def initialize_assembly_activity(self): + activity = sbol2.Activity(f"{self.composite_prefix}_assembly") + + activity.name = "DNA Assembly" + activity.types = "http://sbols.org/v2#build" + + activity_association = sbol2.Association("assemble_") + + assembly_plan = sbol2.Plan("assembly_plan") + + assembly_plan.description = "MoClo DNA Assembly With Opentrons OT2" + + activity_association.plan = assembly_plan + + activity_agent = sbol2.Agent("BuildCompiler") + activity_association.agent = activity_agent + + activity.associations = [activity_association] + + return activity + def rebase_restriction_enzyme(name: str, **kwargs) -> sbol2.ComponentDefinition: """Creates an ComponentDefinition Restriction Enzyme Component from rebase. @@ -749,6 +772,7 @@ def number_to_suffix(n): def ligation( reactants: List[sbol2.ComponentDefinition], assembly_activity: sbol2.Activity, + composite_prefix: str, source_document: sbol2.Document, final_document: sbol2.Document, ligase: sbol2.Implementation, @@ -980,10 +1004,12 @@ def ligation( # create dna component and sequence composite_component_definition, composite_seq = ( dna_componentdefinition_with_sequence( - f"composite_{composite_number}", composite_sequence_str, molecule=True + f"{composite_prefix}_{composite_number}", + composite_sequence_str, + molecule=True, ) ) - composite_component_definition.name = f"composite_{composite_number}" + composite_component_definition.name = f"{composite_prefix}_{composite_number}" composite_component_definition.addRole(ENGINEERED_REGION) composite_component_definition.addType(CIRCULAR) @@ -1004,8 +1030,6 @@ def ligation( prev_part_extract = comp - # _create_precedes_restriction(composite_component_definition, prev_part_extract, composite_component_definition.components[0]) # final component precedes first component; defining circular order - composite_component_definition.sequenceAnnotations = anno_list composite_implementation = sbol2.Implementation( @@ -1060,28 +1084,6 @@ def add_object_to_doc( raise e -def initialize_assembly_activity(): - activity = sbol2.Activity("assembly") - - activity.name = "DNA Assembly" - activity.types = "http://sbols.org/v2#build" - - activity_association = sbol2.Association("assemble_") - - assembly_plan = sbol2.Plan("assembly_plan") - - assembly_plan.description = "MoClo DNA Assembly With Opentrons OT2" - - activity_association.plan = assembly_plan - - activity_agent = sbol2.Agent("BuildCompiler") - activity_association.agent = activity_agent - - activity.associations = [activity_association] - - return activity - - def _create_precedes_restriction( parent_definition: sbol2.ComponentDefinition, subject: sbol2.Component, From 7f68281fdcbf1349c70f84f810ab9f9ae52e74de Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 31 Mar 2026 19:42:22 -0600 Subject: [PATCH 05/93] only grab first and last fusion site from new composite --- src/buildcompiler/plasmid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/buildcompiler/plasmid.py b/src/buildcompiler/plasmid.py index 9b81dfe..d8950ec 100644 --- a/src/buildcompiler/plasmid.py +++ b/src/buildcompiler/plasmid.py @@ -36,7 +36,7 @@ def _match_fusion_sites(self, doc: sbol2.document) -> List[str]: fusion_sites.append(key) fusion_sites.sort() - return fusion_sites + return [fusion_sites[0], fusion_sites[-1]] def _get_antibiotic_resistance(self, doc: sbol2.Document) -> str: for component in ( From 53716c04e85a800d284db8f09d6ccff763bc28ad Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Thu, 2 Apr 2026 16:24:16 -0600 Subject: [PATCH 06/93] codex check --- src/buildcompiler/abstract_translator.py | 2 +- src/buildcompiler/plasmid.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/buildcompiler/abstract_translator.py b/src/buildcompiler/abstract_translator.py index 5c2ef02..5765123 100644 --- a/src/buildcompiler/abstract_translator.py +++ b/src/buildcompiler/abstract_translator.py @@ -90,7 +90,7 @@ def extract_fusion_sites( A list of fusion site component definitions. """ fusion_sites = [] - for component in plasmid.components: + for component in plasmid.getInSequentialOrder(): definition = doc.getComponentDefinition(component.definition) if RESTRICTION_ENZYME_ASSEMBLY_SCAR in definition.roles: fusion_sites.append(definition) diff --git a/src/buildcompiler/plasmid.py b/src/buildcompiler/plasmid.py index d8950ec..1ebb600 100644 --- a/src/buildcompiler/plasmid.py +++ b/src/buildcompiler/plasmid.py @@ -35,8 +35,8 @@ def _match_fusion_sites(self, doc: sbol2.document) -> List[str]: if seq == sequence.upper(): fusion_sites.append(key) - fusion_sites.sort() - return [fusion_sites[0], fusion_sites[-1]] + # fusion_sites.sort() + return fusion_sites def _get_antibiotic_resistance(self, doc: sbol2.Document) -> str: for component in ( From c2fc5c3525a9188e2c3b5bb2926ba3137d64bf65 Mon Sep 17 00:00:00 2001 From: Gonza10V Date: Fri, 3 Apr 2026 16:34:34 -0600 Subject: [PATCH 07/93] linking xml to json and json to protocol --- src/buildcompiler/buildcompiler.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index 01f49f4..607a242 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -9,6 +9,7 @@ get_or_pull, get_compatible_plasmids, ) +from .robotutils import assembly_plan_RDF_to_JSON from .constants import ( AMP, KAN, @@ -337,17 +338,10 @@ def assembly_lvl1( composite_plasmids, product_doc = assembly.run() # TODO upload product_doc? self.indexed_plasmids.extend(composite_plasmids) - + assembly_plan_RDF_to_JSON(product_doc) + return composite_plasmids - # TODO: Create a SBOL representation of the assembly process, updating the SBOL Document. - # Using he selected parts create the representation, you need Plasmids, BsaI and T4 Ligase. - # TODO: Updates indexed plasmids with assembled versions. - # TODO: Generate a protocol for the assembly process. - protocol = "To be implemented by PUDU" - - return protocol - def assembly_lvl2( self, ) -> list[sbol2.ComponentDefinition]: From 5438141af3284263cd3ba4717126667e2b916695 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Fri, 3 Apr 2026 23:24:26 -0600 Subject: [PATCH 08/93] Revise README for clarity and add project purpose Updated project description and added a section explaining the purpose of BuildCompiler in synthetic biology. --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ddaaa1a..cf52666 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # BuildCompiler - BuildCompiler is an open-source tool that bridges the Design and Build stages of the DBTL cycle by compiling SBOL-encoded genetic designs into executable DNA assembly and transformation workflows. + BuildCompiler is an open-source tool that bridges the Design and Build stages of the Synthetic Biology DBTL cycle by compiling standardized genetic designs into executable DNA assembly, transformation, and plating workflows. -It was developed to support build functionality in comand line and cloud workflows in [SynBioSuite](https://synbiosuite.org), based off the [SBOL Best Practices](https://github.com/SynBioDex/SBOL-examples/tree/main/SBOL/best-practices/BP011/). +It supports build functionality in comand line and cloud workflows in [SynBioSuite](https://synbiosuite.org), based off the [SBOL Best Practices](https://github.com/SynBioDex/SBOL-examples/tree/main/SBOL/best-practices/BP011/). BuildCompiler light logo BuildCompiler night logo @@ -12,6 +12,15 @@ It was developed to support build functionality in comand line and cloud workflo ![PyPI - License](https://img.shields.io/pypi/l/sbol2build) ![gh-action badge](https://github.com/MyersResearchGroup/sbol2build/workflows/Python%20package/badge.svg) +## Why this project exists + +Synthetic biology design tools often stop at *design representation*. BuildCompiler closes the gap between: + +1. **Design intent** in SBOL. SBOLCanvas, Cello, and LOICA support SBOL outputs. +2. **Part/backbone selection**. Load your lab inventory to be used by BuildCompiler +3. **Assembly simulation**. Plasmids are digested to generate products by ligation. +4. **Output artifacts**. Protocols, instructions and lab automation scripts to accelerate your workflows. + ## Installing BuildCompiler: ```pip install buildcompiler``` From 45d1a261c4074676cd584d5c4bfd1bfa2a90f11d Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Fri, 3 Apr 2026 23:38:12 -0600 Subject: [PATCH 09/93] Rename project and update metadata in pyproject.toml --- pyproject.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b391319..7130b71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,9 @@ requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] -name = "sbol2build" -version = "0.0.b3" -description = "SBOL2Build: python package for construction of SBOL objects during build planning" +name = "synbio-buildcompiler" +version = "0.0.a1" +description = "BuildCompiler is an open-source tool that bridges the Design and Build stages of the Synthetic Biology DBTL cycle" readme = "README.md" requires-python = ">=3.7" license = {file = "LICENSE.md"} @@ -36,8 +36,8 @@ test = [ ] [project.urls] -"Homepage" = "https://github.com/MyersResearchGroup/SBOL2Build" -"Bug Tracker" = "https://github.com/MyersResearchGroup/SBOL2Build/issues" +"Homepage" = "https://github.com/MyersResearchGroup/BuildCompiler" +"Bug Tracker" = "https://github.com/MyersResearchGroup/BuildCompiler/issues" [dependency-groups] dev = [ From 74fe1d809179204d4d9cc6463bbc6a53d178d121 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Fri, 3 Apr 2026 23:39:31 -0600 Subject: [PATCH 10/93] Delete SBOL2Build.ipynb --- SBOL2Build.ipynb | 2057 ---------------------------------------------- 1 file changed, 2057 deletions(-) delete mode 100644 SBOL2Build.ipynb diff --git a/SBOL2Build.ipynb b/SBOL2Build.ipynb deleted file mode 100644 index b46e23f..0000000 --- a/SBOL2Build.ipynb +++ /dev/null @@ -1,2057 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# SBOL2Build" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import sbol2\n", - "import tyto\n", - "import re\n", - "from Bio import Restriction\n", - "from Bio.Seq import Seq\n", - "from pydna.dseqrecord import Dseqrecord\n", - "from itertools import product\n", - "from typing import Dict, Iterable, List, Union, Optional, Tuple\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "sbol2.Config.setHomespace('https://SBOL2Build.org')" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "metadata": {}, - "outputs": [], - "source": [ - "# this document is used by all the next \n", - "doc = sbol2.Document()\n", - "doc.addNamespace('http://SBOL2Build#', 'SBOL2Build')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Restriction enzyme" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Target function in SBOL3" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#target function\n", - "\n", - "def ed_restriction_enzyme(name:str, **kwargs) -> sbol3.ExternallyDefined:\n", - " \"\"\"Creates an ExternallyDefined Restriction Enzyme Component from rebase.\n", - "\n", - " :param name: Name of the SBOL ExternallyDefined, used by PyDNA. Case sensitive, follow standard restriction enzyme nomenclature, i.e. 'BsaI'\n", - " :param kwargs: Keyword arguments of any other ExternallyDefined attribute.\n", - " :return: An ExternallyDefined object.\n", - " \"\"\"\n", - " check_enzyme = Restriction.__dict__[name]\n", - " definition=f'http://rebase.neb.com/rebase/enz/{name}.html' # TODO: replace with getting the URI from Enzyme when REBASE identifiers become available in biopython 1.8\n", - " return sbol3.ExternallyDefined([sbol3.SBO_PROTEIN], definition=definition, name=name, **kwargs)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## SBOL 2 implementation" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [], - "source": [ - "def rebase_restriction_enzyme(name:str, **kwargs) -> sbol2.ComponentDefinition:\n", - " \"\"\"Creates an ComponentDefinition Restriction Enzyme Component from rebase.\n", - "\n", - " :param name: Name of the SBOL ExternallyDefined, used by PyDNA. Case sensitive, follow standard restriction enzyme nomenclature, i.e. 'BsaI'\n", - " :param kwargs: Keyword arguments of any other ComponentDefinition attribute.\n", - " :return: A ComponentDefinition object.\n", - " \"\"\"\n", - " check_enzyme = Restriction.__dict__[name]\n", - " definition=f'http://rebase.neb.com/rebase/enz/{name}.html' # TODO: replace with getting the URI from Enzyme when REBASE identifiers become available in biopython 1.8\n", - " cd = sbol2.ComponentDefinition(name)\n", - " cd.types = sbol2.BIOPAX_PROTEIN\n", - " cd.name = name\n", - " cd.roles = []\n", - " cd.wasDerivedFrom = definition\n", - " cd.description = f'Restriction enzyme {name} from REBASE.'\n", - " return cd\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Tests" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "bsai = rebase_restriction_enzyme(name=\"BsaI\")" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "BsaI\n" - ] - } - ], - "source": [ - "bsai.wasDerivedFrom\n", - "print(bsai.name)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# DNA ComponentDefinition with Sequence" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Target function in SBOL3" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "ename": "NameError", - "evalue": "name 'sbol3' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[11], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m#target helper function\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mdna_component_with_sequence\u001b[39m(identity: \u001b[38;5;28mstr\u001b[39m, sequence: \u001b[38;5;28mstr\u001b[39m, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Tuple[sbol3\u001b[38;5;241m.\u001b[39mComponent, sbol3\u001b[38;5;241m.\u001b[39mSequence]:\n\u001b[1;32m 3\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Creates a DNA Component and its Sequence.\u001b[39;00m\n\u001b[1;32m 4\u001b[0m \n\u001b[1;32m 5\u001b[0m \u001b[38;5;124;03m :param identity: The identity of the Component. The identity of Sequence is also identity with the suffix '_seq'.\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[38;5;124;03m :return: A tuple of Component and Sequence.\u001b[39;00m\n\u001b[1;32m 9\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[1;32m 10\u001b[0m comp_seq \u001b[38;5;241m=\u001b[39m sbol3\u001b[38;5;241m.\u001b[39mSequence(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00midentity\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m_seq\u001b[39m\u001b[38;5;124m'\u001b[39m, elements\u001b[38;5;241m=\u001b[39msequence, encoding\u001b[38;5;241m=\u001b[39msbol3\u001b[38;5;241m.\u001b[39mIUPAC_DNA_ENCODING)\n", - "\u001b[0;31mNameError\u001b[0m: name 'sbol3' is not defined" - ] - } - ], - "source": [ - "#target helper function\n", - "def dna_component_with_sequence(identity: str, sequence: str, **kwargs) -> Tuple[sbol3.Component, sbol3.Sequence]:\n", - " \"\"\"Creates a DNA Component and its Sequence.\n", - "\n", - " :param identity: The identity of the Component. The identity of Sequence is also identity with the suffix '_seq'.\n", - " :param sequence: The DNA sequence of the Component encoded in IUPAC.\n", - " :param kwargs: Keyword arguments of any other Component attribute.\n", - " :return: A tuple of Component and Sequence.\n", - " \"\"\"\n", - " comp_seq = sbol3.Sequence(f'{identity}_seq', elements=sequence, encoding=sbol3.IUPAC_DNA_ENCODING)\n", - " dna_comp = sbol3.Component(identity, sbol3.SBO_DNA, sequences=[comp_seq], **kwargs)\n", - " return dna_comp, comp_seq\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## SBOL2 implementation" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], - "source": [ - "def dna_componentdefinition_with_sequence2(identity: str, sequence: str, **kwargs) -> Tuple[sbol2.ComponentDefinition, sbol2.Sequence]:\n", - " \"\"\"Creates a DNA ComponentDefinition and its Sequence.\n", - "\n", - " :param identity: The identity of the Component. The identity of Sequence is also identity with the suffix '_seq'.\n", - " :param sequence: The DNA sequence of the Component encoded in IUPAC.\n", - " :param kwargs: Keyword arguments of any other Component attribute.\n", - " :return: A tuple of ComponentDefinition and Sequence.\n", - " \"\"\"\n", - " comp_seq = sbol2.Sequence(f'{identity}_seq', elements=sequence, encoding=sbol2.SBOL_ENCODING_IUPAC)\n", - " dna_comp = sbol2.ComponentDefinition(identity, sbol2.BIOPAX_DNA, **kwargs)\n", - " dna_comp.sequences = [comp_seq]\n", - "\n", - " return dna_comp, comp_seq" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Tests" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Target Part in Backbone from SBOL (do not run)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#target func\n", - "\n", - "def part_in_backbone_from_sbol(identity: Union[str, None], sbol_comp: sbol3.Component, part_location: List[int], part_roles:List[str], fusion_site_length:int, linear:bool=False, **kwargs) -> Tuple[sbol3.Component, sbol3.Sequence]:\n", - " \"\"\"Restructures a non-hierarchical plasmid Component to follow the part-in-backbone pattern following BP011.\n", - " It overwrites the SBOL3 Component provided. \n", - " A part inserted into a backbone is represented by a Component that includes both the part insert \n", - " as a feature that is a SubComponent and the backbone as another SubComponent.\n", - " For more information about BP011 visit https://github.com/SynBioDex/SBOL-examples/tree/main/SBOL/best-practices/BP011 \n", - "\n", - " :param identity: The identity of the Component, is its a String it build a new SBOL Component, if None it adds on top of the input. The identity of Sequence is also identity with the suffix '_seq'.\n", - " :param sbol_comp: The SBOL3 Component that will be used to create the part in backbone Component and Sequence.\n", - " :param part_location: List of 2 integers that indicates the start and the end of the unitary part. Note that the index of the first location is 1, as is typical practice in biology, rather than 0, as is typical practice in computer science.\n", - " :param part_roles: List of strings that indicates the roles to add on the part.\n", - " :param fusion_site_length: Integer of the length of the fusion sites (eg. BsaI fusion site lenght is 4, SapI fusion site lenght is 3)\n", - " :param linear: Boolean than indicates if the backbone is linear, by default it is seted to Flase which means that it has a circular topology. \n", - " :param kwargs: Keyword arguments of any other Component attribute.\n", - " :return: A tuple of Component and Sequence.\n", - " \"\"\"\n", - " if len(part_location) != 2:\n", - " raise ValueError('The part_location only accepts 2 int values in a list.')\n", - " if len(sbol_comp.sequences)!=1:\n", - " raise ValueError(f'The reactant needs to have precisely one sequence. The input reactant has {len(sbol_comp.sequences)} sequences')\n", - " sequence = sbol_comp.sequences[0].lookup().elements\n", - " if identity == None:\n", - " part_in_backbone_component = sbol_comp \n", - " part_in_backbone_seq = sbol_comp.sequences[0]\n", - " else:\n", - " part_in_backbone_component, part_in_backbone_seq = dna_component_with_sequence(identity, sequence, **kwargs)\n", - " part_in_backbone_component.roles.append(sbol3.SO_DOUBLE_STRANDED)\n", - " for part_role in part_roles: \n", - " part_in_backbone_component.roles.append(part_role) \n", - " # creating part feature \n", - " part_location_comp = sbol3.Range(sequence=part_in_backbone_seq, start=part_location[0], end=part_location[1])\n", - " #TODO: add the option of fusion sites to be of different lenghts\n", - " insertion_site_location1 = sbol3.Range(sequence=part_in_backbone_seq, start=part_location[0], end=part_location[0]+fusion_site_length, order=1)\n", - " insertion_site_location2 = sbol3.Range(sequence=part_in_backbone_seq, start=part_location[1]-fusion_site_length, end=part_location[1], order=3)\n", - " part_sequence_feature = sbol3.SequenceFeature(locations=[part_location_comp], roles=part_roles)\n", - " part_sequence_feature.roles.append(tyto.SO.engineered_insert)\n", - " insertion_sites_feature = sbol3.SequenceFeature(locations=[insertion_site_location1, insertion_site_location2], roles=[tyto.SO.insertion_site])\n", - " #TODO: infer topology from the input\n", - " if linear:\n", - " part_in_backbone_component.types.append(sbol3.SO_LINEAR)\n", - " part_in_backbone_component.roles.append(sbol3.SO_ENGINEERED_REGION)\n", - " # creating backbone feature\n", - " open_backbone_location1 = sbol3.Range(sequence=part_in_backbone_seq, start=1, end=part_location[0]+fusion_site_length-1, order=1)\n", - " open_backbone_location2 = sbol3.Range(sequence=part_in_backbone_seq, start=part_location[1]-fusion_site_length, end=len(sequence), order=3)\n", - " open_backbone_feature = sbol3.SequenceFeature(locations=[open_backbone_location1, open_backbone_location2])\n", - " else: \n", - " part_in_backbone_component.types.append(sbol3.SO_CIRCULAR)\n", - " part_in_backbone_component.roles.append(tyto.SO.plasmid_vector)\n", - " # creating backbone feature\n", - " open_backbone_location1 = sbol3.Range(sequence=part_in_backbone_seq, start=1, end=part_location[0]+fusion_site_length-1, order=2)\n", - " open_backbone_location2 = sbol3.Range(sequence=part_in_backbone_seq, start=part_location[1]-fusion_site_length, end=len(sequence), order=1)\n", - " open_backbone_feature = sbol3.SequenceFeature(locations=[open_backbone_location1, open_backbone_location2])\n", - " part_in_backbone_component.features.append(part_sequence_feature)\n", - " part_in_backbone_component.features.append(insertion_sites_feature)\n", - " part_in_backbone_component.features.append(open_backbone_feature)\n", - " backbone_dropout_meets = sbol3.Constraint(restriction='http://sbols.org/v3#meets', subject=part_sequence_feature, object=open_backbone_feature)\n", - " part_in_backbone_component.constraints.append(backbone_dropout_meets)\n", - " #TODO: Add a branch to create a component without overwriting the WHOLE input component\n", - " #removing repeated types and roles\n", - " part_in_backbone_component.types = set(part_in_backbone_component.types)\n", - " part_in_backbone_component.roles = set(part_in_backbone_component.roles)\n", - " return part_in_backbone_component, part_in_backbone_seq" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## SBOL2 Part In Backbone Implementation" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "def part_in_backbone_from_sbol2(identity: Union[str, None], sbol_comp: sbol2.ModuleDefinition, part_location: List[int], part_roles:List[str], fusion_site_length:int, linear:bool=False, **kwargs) -> Tuple[sbol2.ComponentDefinition, sbol2.Sequence]:\n", - " if len(part_location) != 2:\n", - " raise ValueError('The part_location only accepts 2 int values in a list.')\n", - " if len(sbol_comp.sequences)!=1:\n", - " raise ValueError(f'The reactant needs to have precisely one sequence. The input reactant has {len(sbol_comp.sequences)} sequences')\n", - " sequence = doc.find(sbol_comp.sequences[0]).elements\n", - " if identity == None:\n", - " part_in_backbone_component = sbol_comp \n", - " part_in_backbone_seq = doc.find(sbol_comp.sequences[0]).elements\n", - " part_in_backbone_component.sequences = [part_in_backbone_seq]\n", - " else:\n", - " part_in_backbone_component, part_in_backbone_seq = dna_componentdefinition_with_sequence2(identity, sequence, **kwargs)\n", - " # double stranded\n", - " part_in_backbone_component.addRole('http://identifiers.org/SO:0000985')\n", - " for part_role in part_roles: \n", - " part_in_backbone_component.addRole(part_role)\n", - "\n", - " # creating part annotation \n", - " part_location_comp = sbol2.Range( start=part_location[0], end=part_location[1])\n", - " insertion_site_location1 = sbol2.Range( uri=\"insertloc1\", start=part_location[0], end=part_location[0]+fusion_site_length) #order 1\n", - " insertion_site_location2 = sbol2.Range( uri=\"insertloc2\", start=part_location[1]-fusion_site_length, end=part_location[1]) #order 3\n", - "\n", - " part_sequence_annotation = sbol2.SequenceAnnotation('part_sequence_annotation')\n", - " part_sequence_annotation.roles = part_roles\n", - " part_sequence_annotation.locations.add(part_location_comp)\n", - "\n", - " part_sequence_annotation.addRole(tyto.SO.engineered_insert)\n", - " insertion_sites_annotation = sbol2.SequenceAnnotation('insertion_sites_annotation')\n", - "\n", - " insertion_sites_annotation.locations.add(insertion_site_location1)\n", - " insertion_sites_annotation.locations.add(insertion_site_location2)\n", - " \n", - " insertion_sites_annotation.roles = [tyto.SO.insertion_site]\n", - " if linear:\n", - " part_in_backbone_component.addRole('http://identifiers.org/SO:0000987') #linear\n", - " part_in_backbone_component.addRole('http://identifiers.org/SO:0000804') #engineered region\n", - " # creating backbone feature\n", - " open_backbone_location1 = sbol2.Range(start=1, end=part_location[0]+fusion_site_length-1) #order 1\n", - " open_backbone_location2 = sbol2.Range(start=part_location[1]-fusion_site_length, end=len(sequence)) #order 3\n", - " open_backbone_annotation = sbol2.SequenceAnnotation(locations=[open_backbone_location1, open_backbone_location2])\n", - " else: \n", - " part_in_backbone_component.addRole('http://identifiers.or/SO:0000988') #circular\n", - " part_in_backbone_component.addRole(tyto.SO.plasmid_vector)\n", - " # creating backbone feature\n", - " open_backbone_location1 = sbol2.Range( uri=\"backboneloc1\", start=1, end=part_location[0]+fusion_site_length-1 ) #order 2\n", - " open_backbone_location2 = sbol2.Range( uri=\"backboneloc2\", start=part_location[1]-fusion_site_length, end=len(sequence)) #order 1\n", - " open_backbone_annotation = sbol2.SequenceAnnotation('open_backbone_annotation')\n", - " open_backbone_annotation.locations.add(open_backbone_location1)\n", - " open_backbone_annotation.locations.add(open_backbone_location2)\n", - " \n", - " part_in_backbone_component.sequenceAnnotations.add(part_sequence_annotation)\n", - " part_in_backbone_component.sequenceAnnotations.add(insertion_sites_annotation)\n", - " part_in_backbone_component.sequenceAnnotations.add(open_backbone_annotation) \n", - " # use sequenceconstrait with precedes\n", - " # backbone_dropout_meets = sbol3.Constraint(restriction='http://sbols.org/v3#meets', subject=part_sequence_annotation, object=open_backbone_annotation) #????\n", - " backbone_dropout_meets = sbol2.sequenceconstraint.SequenceConstraint(uri='backbone_dropout_meets', restriction=sbol2.SBOL_RESTRICTION_PRECEDES) #might need to add uri as param 2\n", - " backbone_dropout_meets.subject = part_sequence_annotation\n", - " backbone_dropout_meets.object = open_backbone_annotation\n", - " \n", - " part_in_backbone_component.sequenceConstraints.add(backbone_dropout_meets)\n", - " #TODO: Add a branch to create a component without overwriting the WHOLE input component\n", - " #removing repeated types and roles\n", - " part_in_backbone_component.types = set(part_in_backbone_component.types)\n", - " part_in_backbone_component.roles = set(part_in_backbone_component.roles)\n", - " return part_in_backbone_component, part_in_backbone_seq" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# benchling plasmid test\n", - "doc = sbol2.Document()\n", - "benchling_comp = sbol2.ComponentDefinition('benchling_comp', sbol2.BIOPAX_DNA)\n", - "\n", - "benchSeq = sbol2.Sequence('benchSeq', 'tcattgccatacgaaattccggatgagcattcatcaggcgggcaagaatgtgaataaaggccggataaaacttgtgcttatttttctttacggtctttaaaaaggccgtaatatccagctgaacggtctggttataggtacattgagcaactgactgaaatgcctcaaaatgttctttacgatgccattgggatatatcaacggtggtatatccagtgatttttttctccattttagcttccttagctcctgaaaatctcgataactcaaaaaatacgcccggtagtgatcttatttcattatggtgaaagttggaacctcttacgtgcccgatcaactcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcggtctcgGGAGtttacagctagctcagtcctaggtattatgctagcTACTCGAGaccctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgaggcttggattctcaccaataaaaaacgcccggcggcaaccgagcgttctgaacaaatccagatggagttctgaggtcattactggatctatcaacaggagtccaagcgagctcgatatcaaattacgccccgccctgccactcatcgcagtactgttgtaattcattaagcattctgccgacatggaagccatcacaaacggcatgatgaacctgaatcgccagcggcatcagcaccttgtcgccttgcgtataatatttgcccatggtgaaaacgggggcgaagaagttgtccatattggccacgtttaaatcaaaactggtgaaactcacccagggattggctgagacgaaaaacatattctcaataaaccctttagggaaataggccaggttttcaccgtaacacgccacatcttgcgaatatatgtgtagaaactgccggaaatcgtcgtggtattcactccagagcgatgaaaacgtttcagtttgctcatggaaaacggtgtaacaagggtgaacactatcccatatcaccagctcaccgtct', sbol2.SBOL_ENCODING_IUPAC)\n", - "\n", - "doc.addComponentDefinition(benchling_comp)\n", - "doc.addSequence(benchSeq)\n", - "benchling_comp.sequences = [benchSeq] \n", - "\n", - "resultComponent, resultSequence = part_in_backbone_from_sbol2(identity=\"benchling_comp\", sbol_comp=benchling_comp, part_location=[531, 602], part_roles=[], fusion_site_length=6)\n", - "\n", - "doc2 = sbol2.Document()\n", - "\n", - "doc2.addComponentDefinition(resultComponent)\n", - "doc2.addComponentDefinition(resultSequence)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Invalid. sbol-11403:\\x00 Strong Validation Error:\\x00 The Component referenced by the subject property of a SequenceConstraint MUST be contained by the ComponentDefinition that contains the SequenceConstraint. \\x00Reference: SBOL Version 2.3.0 Section 7.7.6 on page 36 :\\x00 http://examples.org/ComponentDefinition/benchling_comp/backbone_dropout_meets/1\\x00 Validation failed.'" - ] - }, - "execution_count": 156, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "doc2.write('example1.xml')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Initialize SBOL2 Document\n", - "sbol2.Config.setOption('sbol_typed_uris', True)\n", - "doc = sbol2.Document()\n", - "\n", - "# Create a ComponentDefinition with a DNA sequence\n", - "component_def = sbol2.ComponentDefinition('example_component', sbol2.BIOPAX_DNA)\n", - "doc.addComponentDefinition(component_def)\n", - "\n", - "# Add a sequence to the ComponentDefinition\n", - "sequence = sbol2.Sequence('example_sequence', 'ATGCTGACTGCTAGCTGACTAGC', sbol2.SBOL_ENCODING_IUPAC)\n", - "doc.addSequence(sequence)\n", - "component_def.sequences = [sequence.identity] # Associate the sequence with the ComponentDefinition\n", - "\n", - "# Create a SequenceAnnotation\n", - "seq_annotation = sbol2.SequenceAnnotation('example_annotation')\n", - "range_location = sbol2.Range('new range', 1, 6) # Create a Range object\n", - "seq_annotation.locations.add(range_location) # Add it to the annotation\n", - "component_def.sequenceAnnotations.add(seq_annotation)\n", - "\n", - "# Optionally add a role (e.g., promoter, CDS) to describe the feature\n", - "seq_annotation.roles = ['http://identifiers.org/SO:0000167'] # Promoter role from Sequence Ontology\n", - "\n", - "# Save the SBOL2 document\n", - "doc.write('sequence_annotation_example.xml')\n", - "\n", - "print('SequenceAnnotation created successfully!')\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "component_def = sbol2.ComponentDefinition('example_component', sbol2.BIOPAX_DNA)\n", - "sequence = sbol2.Sequence('example_sequence', 'ATGCTGACTGCTAGCTGACTAGC', sbol2.SBOL_ENCODING_IUPAC)\n", - "component_def.sequences = [sequence.identity] # Associate the sequence with the ComponentDefinition\n", - "\n", - "\n", - "anno_1 = sbol2.SequenceAnnotation('new_anno')\n", - "anno_2 = sbol2.SequenceAnnotation('new_anno_2')\n", - "\n", - "range_location = sbol2.Range('new range', 1, 6) # Create a Range object\n", - "anno_1.locations.add(range_location) # Add it to the annotation)\n", - "\n", - "component_def.sequenceAnnotations.add(anno_1)\n", - "component_def.sequenceAnnotations.add(anno_2)\n", - "\n", - "print(sbol2.SBOL_RESTRICTION_PRECEDES)\n", - "\n", - "# new_constraint = sbol2.SequenceConstraint(restriction=SBOL_RESTRICTION_PRECEDES, subject=anno_1, object=anno_2) #????\n", - "backbone_dropout_meets = sbol2.sequenceconstraint.SequenceConstraint (\n", - " 'backbone_dropout',\n", - " 'backbone_dropout_uri',\n", - " anno_1,\n", - " anno_2,\n", - " sbol2.SBOL_RESTRICTION_PRECEDES\n", - ")\n", - "\n", - "component_def.sequenceConstraints.add(backbone_dropout_meets)\n", - "\n", - "sbol2.DNA" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#target func\n", - "class Assembly_plan_composite_in_backbone_single_enzyme():\n", - " \"\"\"Creates a Assembly Plan.\n", - " :param name: Name of the assembly plan Component.\n", - " :param parts_in_backbone: Parts in backbone to be assembled. \n", - " :param acceptor_backbone: Backbone in which parts are inserted on the assembly. \n", - " :param restriction_enzymes: Restriction enzyme with correct name from Bio.Restriction as Externally Defined.\n", - " :param document: SBOL Document where the assembly plan will be created.\n", - " :param linear: Boolean to inform if the reactant is linear.\n", - " :param circular: Boolean to inform if the reactant is circular.\n", - " :param **kwargs: Keyword arguments of any other Component attribute for the assembled part.\n", - " \"\"\"\n", - "\n", - " def __init__(self, name: str, parts_in_backbone: List[sbol3.Component], acceptor_backbone: sbol3.Component, restriction_enzyme: Union[str,sbol3.ExternallyDefined], document:sbol3.Document):\n", - " self.name = name\n", - " self.parts_in_backbone = parts_in_backbone\n", - " self.acceptor_backbone = acceptor_backbone\n", - " self.restriction_enzyme = restriction_enzyme\n", - " self.products = []\n", - " self.extracted_parts = []\n", - " self.document = document\n", - "\n", - " #create assembly plan\n", - " self.assembly_plan_component = sbol3.Component(identity=f'{self.name}_assembly_plan', types=sbol3.SBO_FUNCTIONAL_ENTITY)\n", - " self.document.add(self.assembly_plan_component)\n", - " self.composites = []\n", - "\n", - " def run(self):\n", - " self.assembly_plan_component.features.append(self.restriction_enzyme)\n", - " #extract parts\n", - " part_number = 1\n", - " for part_in_backbone in self.parts_in_backbone:\n", - " part_comp, part_seq = digestion(reactant=part_in_backbone,restriction_enzymes=[self.restriction_enzyme], assembly_plan=self.assembly_plan_component, name=f'part_{part_number}_{part_in_backbone.display_id}')\n", - " self.document.add([part_comp, part_seq])\n", - " self.extracted_parts.append(part_comp)\n", - " part_number += 1\n", - " #extract backbone (should be the same?)\n", - " backbone_comp, backbone_seq = digestion(reactant=self.acceptor_backbone,restriction_enzymes=[self.restriction_enzyme], assembly_plan=self.assembly_plan_component, name=f'part_{part_number}')\n", - " self.document.add([backbone_comp, backbone_seq])\n", - " self.extracted_parts.append(backbone_comp)\n", - " \n", - " #create composite part from extracted parts\n", - " composites_list = ligation(reactants=self.extracted_parts, assembly_plan=self.assembly_plan_component)\n", - " for composite in composites_list:\n", - " composite[0].generated_by.append(self.assembly_plan_component) #\n", - " self.composites.append(composite)\n", - " self.products.append(composite)\n", - " self.document.add(composite)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Digestion" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Target function in SBOL3 (do not run)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def digestion(reactant:sbol3.Component, restriction_enzymes:List[sbol3.ExternallyDefined], assembly_plan:sbol3.Component, **kwargs)-> Tuple[sbol3.Component, sbol3.Sequence]:\n", - " \"\"\"Digests a Component using the provided restriction enzymes and creates a product Component and a digestion Interaction.\n", - " The product Component is assumed to be the insert for parts in backbone and the backbone for backbones.\n", - "\n", - " :param reactant: DNA to be digested as SBOL Component, usually a part_in_backbone. \n", - " :param restriction_enzymes: Restriction enzymes used Externally Defined.\n", - " :return: A tuple of Component and Interaction.\n", - " \"\"\"\n", - " if sbol3.SBO_DNA not in reactant.types:\n", - " raise TypeError(f'The reactant should has a DNA type. Types founded {reactant.types}.')\n", - " if len(reactant.sequences)!=1:\n", - " raise ValueError(f'The reactant needs to have precisely one sequence. The input reactant has {len(reactant.sequences)} sequences')\n", - " participations=[]\n", - " restriction_enzymes_pydna=[] \n", - " for re in restriction_enzymes:\n", - " enzyme = Restriction.__dict__[re.name]\n", - " restriction_enzymes_pydna.append(enzyme)\n", - " modifier_participation = sbol3.Participation(roles=[sbol3.SBO_MODIFIER], participant=re)\n", - " participations.append(modifier_participation)\n", - "\n", - " # Inform topology to PyDNA, if not found assuming linear. \n", - " if is_circular(reactant):\n", - " circular=True\n", - " linear=False\n", - " else: \n", - " circular=False\n", - " linear=True\n", - " \n", - " reactant_seq = reactant.sequences[0].lookup().elements\n", - " # Dseqrecord is from PyDNA package with reactant sequence\n", - " ds_reactant = Dseqrecord(reactant_seq, linear=linear, circular=circular)\n", - " digested_reactant = ds_reactant.cut(restriction_enzymes_pydna)\n", - "\n", - " if len(digested_reactant)<2 or len(digested_reactant)>3:\n", - " raise NotImplementedError(f'Not supported number of products. Found{len(digested_reactant)}')\n", - " #TODO select them based on content rather than size.\n", - " elif circular and len(digested_reactant)==2:\n", - " part_extract, backbone = sorted(digested_reactant, key=len)\n", - " elif linear and len(digested_reactant)==3:\n", - " prefix, part_extract, suffix = digested_reactant\n", - " else: raise NotImplementedError('The reactant has no valid topology type')\n", - " \n", - " # Extracting roles from features\n", - " reactant_features_roles = []\n", - " for f in reactant.features:\n", - " for r in f.roles:\n", - " reactant_features_roles.append(r)\n", - " # if part\n", - " if any(n==tyto.SO.engineered_insert for n in reactant_features_roles):\n", - " # Compute the length of single strand sticky ends or fusion sites\n", - " product_5_prime_ss_strand, product_5_prime_ss_end = part_extract.seq.five_prime_end()\n", - " product_3_prime_ss_strand, product_3_prime_ss_end = part_extract.seq.three_prime_end()\n", - " \n", - " product_sequence = str(part_extract.seq)\n", - " prod_component_definition, prod_seq = dna_component_with_sequence(identity=f'{reactant.name}_part_extract', sequence=product_sequence, **kwargs) #str(product_sequence))\n", - " # add sticky ends features\n", - " five_prime_fusion_site_location = sbol3.Range(sequence=product_sequence, start=1, end=len(product_5_prime_ss_end), order=1)\n", - " three_prime_fusion_site_location = sbol3.Range(sequence=product_sequence, start=len(product_sequence)-len(product_3_prime_ss_end)+1, end=len(product_sequence), order=3)\n", - " fusion_sites_feature = sbol3.SequenceFeature(locations=[five_prime_fusion_site_location, three_prime_fusion_site_location], roles=[tyto.SO.insertion_site])\n", - " prod_component_definition.roles.append(tyto.SO.engineered_insert) \n", - " prod_component_definition.features.append(fusion_sites_feature)\n", - "\n", - " # if backbone\n", - " elif any(n==tyto.SO.deletion for n in reactant_features_roles):\n", - " # Compute the length of single strand sticky ends or fusion sites\n", - " product_5_prime_ss_strand, product_5_prime_ss_end = backbone.seq.five_prime_end()\n", - " product_3_prime_ss_strand, product_3_prime_ss_end = backbone.seq.three_prime_end()\n", - " product_sequence = str(backbone.seq)\n", - " prod_component_definition, prod_seq = dna_component_with_sequence(identity=f'{reactant.name}_backbone', sequence=product_sequence, **kwargs) #str(product_sequence))\n", - " # add sticky ends features\n", - " five_prime_fusion_site_location = sbol3.Range(sequence=product_sequence, start=1, end=len(product_5_prime_ss_end), order=1)\n", - " three_prime_fusion_site_location = sbol3.Range(sequence=product_sequence, start=len(product_sequence)-len(product_3_prime_ss_end)+1, end=len(product_sequence), order=3)\n", - " fusion_sites_feature = sbol3.SequenceFeature(locations=[five_prime_fusion_site_location, three_prime_fusion_site_location], roles=[tyto.SO.insertion_site])\n", - " prod_component_definition.roles.append(tyto.SO.plasmid_vector)\n", - " prod_component_definition.features.append(fusion_sites_feature)\n", - "\n", - " else: raise NotImplementedError('The reactant has no valid roles')\n", - "\n", - " #Add reference to part in backbone\n", - " reactant_subcomponent = sbol3.SubComponent(reactant)\n", - " prod_component_definition.features.append(reactant_subcomponent)\n", - " # Create reactant Participation.\n", - " react_subcomp = sbol3.SubComponent(reactant)\n", - " assembly_plan.features.append(react_subcomp)\n", - " reactant_participation = sbol3.Participation(roles=[sbol3.SBO_REACTANT], participant=react_subcomp)\n", - " participations.append(reactant_participation)\n", - " \n", - " prod_subcomp = sbol3.SubComponent(prod_comp)\n", - " assembly_plan.features.append(prod_subcomp)\n", - " product_participation = sbol3.Participation(roles=[sbol3.SBO_PRODUCT], participant=prod_subcomp)\n", - " participations.append(product_participation)\n", - " \n", - " # Make Interaction\n", - " interaction = sbol3.Interaction(types=[tyto.SBO.cleavage], participations=participations)\n", - " assembly_plan.interactions.append(interaction)\n", - " \n", - " return prod_comp, prod_seq" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# SBOL2 implementation" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Is Ciruclar" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": {}, - "outputs": [], - "source": [ - "# helper function\n", - "def is_circular(obj: sbol2.ComponentDefinition) -> bool:\n", - " \"\"\"Check if an SBOL Component or Feature is circular.\n", - " :param obj: design to be checked\n", - " :return: true if circular\n", - " \"\"\" \n", - " return any(n==sbol2.SO_CIRCULAR for n in obj.types)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Tests" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "All tests passed!\n" - ] - } - ], - "source": [ - "import unittest\n", - "\n", - "cd1 = sbol2.ComponentDefinition(\"circular_cd\")\n", - "cd1.types = [sbol2.SO_CIRCULAR]\n", - "assert is_circular(cd1), \"Circular ComponentDefinition should return True\"\n", - "\n", - "cd2 = sbol2.ComponentDefinition(\"non_circular_cd\")\n", - "cd2.types = [sbol2.BIOPAX_DNA]\n", - "assert not is_circular(cd2), \"Non-circular ComponentDefinition should return False\"\n", - "\n", - "cd3 = sbol2.ComponentDefinition(\"empty_types\")\n", - "assert not is_circular(cd3), \"Empty types list should return False\"\n", - "\n", - "print(\"All tests passed!\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Part Digestion" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [], - "source": [ - "def part_digestion(reactant:sbol2.ModuleDefinition, restriction_enzymes:List[sbol2.ComponentDefinition], assembly_plan:sbol2.ModuleDefinition, document: sbol2.Document, **kwargs)-> Tuple[List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]], sbol2.ModuleDefinition]:\n", - " \"\"\"Digests a ModuleDefinition using the provided restriction enzymes and creates a product ComponentDefinition and a digestion Interaction.\n", - " The product ComponentDefinition is assumed to be the insert for parts in backbone and the open backbone for backbones.\n", - "\n", - " :param reactant: DNA to be digested as SBOL ModuleDefinition, usually a part_in_backbone. \n", - " :param restriction_enzymes: Restriction enzymes used ComponentDefinition.\n", - " :param assembly_plan: SBOL ModuleDefinition to contain the functional components, interactions, and participants\n", - " :param document: SBOL2 document to be used to extract referenced objects.\n", - " :return: A list of tuples of [ComponentDefinition, Sequence], and an assemblyplan ModuleDefinition.\n", - " \"\"\"\n", - " # extract component definition from module\n", - " reactant_displayId = reactant.functionalComponents[0].displayId\n", - " reactant_def_URI = reactant.functionalComponents[0].definition\n", - " reactant_component_definition = document.getComponentDefinition(reactant_def_URI)\n", - "\n", - " if sbol2.BIOPAX_DNA not in reactant_component_definition.types:\n", - " raise TypeError(f'The reactant should has a DNA type. Types found {reactant.types}.')\n", - " if len(reactant_component_definition.sequences)!=1:\n", - " raise ValueError(f'The reactant needs to have precisely one sequence. The input reactant has {len(reactant.sequences)} sequences')\n", - " participations=[]\n", - " extracts_list=[]\n", - " restriction_enzymes_pydna=[] \n", - " \n", - " for re in restriction_enzymes:\n", - " enzyme = Restriction.__dict__[re.name]\n", - " restriction_enzymes_pydna.append(enzyme)\n", - "\n", - " enzyme_component = sbol2.FunctionalComponent(uri=f\"{re.name}_enzyme\")\n", - " enzyme_component.definition = re\n", - " enzyme_component.displayID = f\"{re.name}_enzyme\"\n", - " enzyme_in_module = False\n", - "\n", - " for comp in assembly_plan.functionalComponents:\n", - " if (comp.displayId == enzyme_component.displayID): \n", - " enzyme_component = comp\n", - " enzyme_in_module = True\n", - "\n", - " if not enzyme_in_module:\n", - " assembly_plan.functionalComponents.add(enzyme_component)\n", - " \n", - " modifier_participation = sbol2.Participation(uri='restriction')\n", - " modifier_participation.participant = enzyme_component\n", - " modifier_participation.roles = ['http://identifiers.org/biomodels.sbo/SBO:0000019']\n", - " participations.append(modifier_participation)\n", - "\n", - " # Inform topology to PyDNA, if not found assuming linear. \n", - " if is_circular(reactant_component_definition):\n", - " circular=True\n", - " linear=False\n", - " else: \n", - " circular=False\n", - " linear=True\n", - " \n", - " reactant_seq = reactant_component_definition.sequences[0]\n", - " reactant_seq = document.getSequence(reactant_seq).elements\n", - " # Dseqrecord is from PyDNA package with reactant sequence\n", - " ds_reactant = Dseqrecord(reactant_seq, circular=circular)\n", - " digested_reactant = ds_reactant.cut(restriction_enzymes_pydna) #TODO see if ds_reactant.cut is working, causing problems downstream\n", - "\n", - "\n", - " if len(digested_reactant)<2 or len(digested_reactant)>3: \n", - " raise NotImplementedError(f'Not supported number of products. Found{len(digested_reactant)}')\n", - " #TODO select them based on content rather than size.\n", - " elif circular and len(digested_reactant)==2:\n", - " part_extract, backbone = sorted(digested_reactant, key=len)\n", - " elif linear and len(digested_reactant)==3:\n", - " prefix, part_extract, suffix = digested_reactant\n", - " else: raise NotImplementedError('The reactant has no valid topology type')\n", - "\n", - " # Compute the length of single strand sticky ends or fusion sites\n", - " product_5_prime_ss_strand, product_5_prime_ss_end = part_extract.seq.five_prime_end()\n", - " product_3_prime_ss_strand, product_3_prime_ss_end = part_extract.seq.three_prime_end()\n", - " product_sequence = str(part_extract.seq)\n", - " prod_component_definition, prod_seq = dna_componentdefinition_with_sequence2(identity=f'{reactant.functionalComponents[0].displayId}_extracted_part', sequence=product_sequence, **kwargs)\n", - " prod_component_definition.wasDerivedFrom = reactant_component_definition.identity\n", - " extracts_list.append((prod_component_definition, prod_seq))\n", - "\n", - " # five prime overhang\n", - " five_prime_oh_definition = sbol2.ComponentDefinition(uri=f\"{reactant_displayId}_five_prime_oh\") # TODO: ensure circular type is preserved for sbh visualization\n", - " five_prime_oh_definition.addRole('http://identifiers.org/so/SO:0001932')\n", - " five_prime_oh_location = sbol2.Range(uri=\"five_prime_oh_location\", start=1, end=len(product_5_prime_ss_end))\n", - " five_prime_oh_component = sbol2.Component(uri=f\"{reactant_displayId}_five_prime_oh_component\")\n", - " five_prime_oh_component.definition = five_prime_oh_definition\n", - " five_prime_overhang_annotation = sbol2.SequenceAnnotation(uri=\"five_prime_overhang\")\n", - " five_prime_overhang_annotation.locations.add(five_prime_oh_location)\n", - "\n", - " # extracted part => point straight to part from sbolcanvas\n", - " part_location = sbol2.Range(uri=f\"{reactant_displayId}_part_location\", start=len(product_5_prime_ss_end)+1, end=len(product_sequence)-len(product_3_prime_ss_end))\n", - " part_extract_annotation = sbol2.SequenceAnnotation(uri=f\"{reactant_displayId}_part\")\n", - " part_extract_annotation.locations.add(part_location)\n", - "\n", - " # three prime overhang\n", - " three_prime_oh_definition = sbol2.ComponentDefinition(uri=f\"{reactant_displayId}_three_prime_oh\")\n", - " three_prime_oh_definition.addRole('http://identifiers.org/so/SO:0001933')\n", - " three_prime_oh_location = sbol2.Range(uri=\"three_prime_oh_location\", start=len(product_sequence)-len(product_3_prime_ss_end)+1, end=len(product_sequence)) \n", - " three_prime_oh_component = sbol2.Component(uri=f\"{reactant_displayId}_three_prime_oh_component\")\n", - " three_prime_oh_component.definition = three_prime_oh_definition\n", - " three_prime_overhang_annotation = sbol2.SequenceAnnotation(uri=\"three_prime_overhang\")\n", - " three_prime_overhang_annotation.locations.add(three_prime_oh_location)\n", - " \n", - " # three_prime_overhang_annotation.addRole(tyto.SO.insertion_site) #TODO: do we need this, or change to three_prime_overhang_annotation.addRole('http://identifiers.org/SO:0000366')?\n", - "\n", - " anno_component_dict = {}\n", - "\n", - " prod_component_definition.components = [five_prime_oh_component, three_prime_oh_component]\n", - " three_prime_overhang_annotation.component = three_prime_oh_component\n", - " five_prime_overhang_annotation.component = five_prime_oh_component\n", - "\n", - " original_part_def_URI = \"\"\n", - "\n", - " #enccode ontologies of overhangs\n", - " for definition in document.componentDefinitions:\n", - " for seqURI in definition.sequences:\n", - " seq = document.getSequence(seqURI)\n", - " if seq.elements.lower() == Seq(product_3_prime_ss_end).reverse_complement():\n", - " three_prime_oh_definition.wasDerivedFrom = definition.identity\n", - " three_prime_sequence = sbol2.Sequence(uri=f\"{three_prime_oh_definition.displayId}_sequence\", elements= seq.elements)\n", - " three_prime_sequence.wasDerivedFrom = seq.identity\n", - " three_prime_oh_definition.sequences = [three_prime_sequence]\n", - " three_prime_oh_definition.types.append(\"http://identifiers.org/SO:0000984\") # single-stranded for overhangs\n", - " \n", - " extracts_list.append((three_prime_oh_definition, three_prime_sequence))\n", - " extracts_list.append((definition, seq)) #add scars to list\n", - "\n", - " elif seq.elements.lower() == product_sequence[4:-4]: \n", - " original_part_def_URI = definition.identity\n", - " extracts_list.append((definition, seq))\n", - "\n", - " elif seq.elements.lower() == product_5_prime_ss_end:\n", - " five_prime_oh_definition.wasDerivedFrom = definition.identity\n", - " five_prime_sequence = sbol2.Sequence(uri=f\"{five_prime_oh_definition.displayId}_sequence\", elements= seq.elements)\n", - " five_prime_sequence.wasDerivedFrom = seq.identity\n", - " five_prime_oh_definition.sequences = [five_prime_sequence]\n", - " five_prime_oh_definition.types.append(\"http://identifiers.org/SO:0000984\") # single-stranded for overhangs\n", - "\n", - " extracts_list.append((five_prime_oh_definition, five_prime_sequence))\n", - " extracts_list.append((definition, seq))\n", - "\n", - " #find + add original component to product def & annotation\n", - " for comp in reactant_component_definition.components: \n", - " if comp.definition == original_part_def_URI:\n", - " prod_component_definition.components.add(comp)\n", - " part_extract_annotation.component = comp\n", - "\n", - " prod_component_definition.sequenceAnnotations.add(three_prime_overhang_annotation)\n", - " prod_component_definition.sequenceAnnotations.add(five_prime_overhang_annotation)\n", - " prod_component_definition.sequenceAnnotations.add(part_extract_annotation)\n", - " prod_component_definition.addRole(tyto.SO.engineered_insert) \n", - " prod_component_definition.addType('http://identifiers.org/so/SO:0000987')\n", - "\n", - " #Add reference to part in backbone\n", - " reactant_component = sbol2.FunctionalComponent(uri=f\"{reactant_displayId}_reactant\")\n", - " reactant_component.definition = reactant_component_definition\n", - " assembly_plan.functionalComponents.add(reactant_component)\n", - "\n", - " # Create reactant Participation.\n", - " reactant_participation = sbol2.Participation(uri=f\"{reactant_displayId}_reactant\")\n", - " reactant_participation.participant = reactant_component\n", - " reactant_participation.roles = [sbol2.SBO_REACTANT]\n", - " participations.append(reactant_participation)\n", - " \n", - " prod_component = sbol2.FunctionalComponent(uri=f\"{reactant_displayId}_digestion_product\")\n", - " prod_component.definition = prod_component_definition\n", - " assembly_plan.functionalComponents.add(prod_component)\n", - "\n", - " product_participation = sbol2.Participation(uri=f\"{reactant_displayId}_product\")\n", - " product_participation.participant = prod_component\n", - " product_participation.roles = [sbol2.SBO_PRODUCT]\n", - " participations.append(product_participation)\n", - " \n", - " # Make Interaction\n", - " interaction = sbol2.Interaction(uri=f\"{reactant_displayId}_digestion_interaction\", interaction_type='https://identifiers.org/biomodels.sbo/SBO:0000178')\n", - " interaction.participations = participations\n", - " assembly_plan.interactions.add(interaction)\n", - " \n", - " return extracts_list, assembly_plan" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Tests" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "http://examples.org/ComponentDefinition/UJHDBOTD_70_extracted_part/1\n", - "http://examples.org/Sequence/UJHDBOTD_70_extracted_part_seq/1\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_70_three_prime_oh/1\n", - "http://examples.org/Sequence/UJHDBOTD_70_three_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_B/1\n", - "https://sbolcanvas.org/Scar_B_sequence\n", - "https://sbolcanvas.org/J23101/1\n", - "https://sbolcanvas.org/J23101_sequence\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_70_five_prime_oh/1\n", - "http://examples.org/Sequence/UJHDBOTD_70_five_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_A/1\n", - "https://sbolcanvas.org/Scar_A_sequence\n" - ] - }, - { - "data": { - "text/plain": [ - "'Valid.'" - ] - }, - "execution_count": 117, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "pro_in_bb_doc = sbol2.Document()\n", - "pro_in_bb_doc.read('tests/test_files/test_files/pro_in_bb.xml')\n", - "md = pro_in_bb_doc.getModuleDefinition(\"https://sbolcanvas.org/module1\")\n", - "assembly_plan = sbol2.ModuleDefinition('insert_assembly_plan')\n", - "\n", - "extracts_tuple_list, assembly_plan = part_digestion(md, [bsai], assembly_plan, pro_in_bb_doc)\n", - "part_extract, seq = extracts_tuple_list[0]\n", - "\n", - "new_doc = sbol2.Document()\n", - "for extract, sequence in extracts_tuple_list:\n", - " print(extract.identity)\n", - " print(sequence.identity)\n", - " new_doc.add(extract)\n", - " new_doc.add(sequence)\n", - "new_doc.add(assembly_plan)\n", - "new_doc.write('new_promoter_test.xml')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Backbone Digestion" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "def backbone_digestion(reactant:sbol2.ModuleDefinition, restriction_enzymes:List[sbol2.ComponentDefinition], assembly_plan:sbol2.ModuleDefinition, document: sbol2.Document, **kwargs)-> Tuple[List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]], sbol2.ModuleDefinition]:\n", - " \"\"\"Digests a ModuleDefinition using the provided restriction enzymes and creates a product ComponentDefinition and a digestion Interaction.\n", - " The product ComponentDefinition is assumed to be the insert for parts in backbone and the open backbone for backbones.\n", - "\n", - " :param reactant: DNA to be digested as SBOL ModuleDefinition, usually a part_in_backbone. \n", - " :param restriction_enzymes: Restriction enzymes used ComponentDefinition.\n", - " :param assembly_plan: SBOL ModuleDefinition to contain the functional components, interactions, and participants\n", - " :param document: SBOL2 document to be used to extract referenced objects.\n", - " :return: A tuple of ComponentDefinition, Sequence, and ModuleDefinition.\n", - " \"\"\"\n", - " # extract component definition from module\n", - " reactant_displayId = reactant.functionalComponents[0].displayId\n", - " reactant_def_URI = reactant.functionalComponents[0].definition\n", - " reactant_component_definition = document.getComponentDefinition(reactant_def_URI)\n", - "\n", - " if sbol2.BIOPAX_DNA not in reactant_component_definition.types:\n", - " raise TypeError(f'The reactant should has a DNA type. Types founded {reactant.types}.')\n", - " if len(reactant_component_definition.sequences)!=1: # TODO review if true for MD, maybe for MD it will be 5\n", - " raise ValueError(f'The reactant needs to have precisely one sequence. The input reactant has {len(reactant.sequences)} sequences')\n", - " participations=[]\n", - " extracts_list=[]\n", - " restriction_enzymes_pydna=[] \n", - " \n", - " for re in restriction_enzymes:\n", - " enzyme = Restriction.__dict__[re.name]\n", - " restriction_enzymes_pydna.append(enzyme)\n", - "\n", - " enzyme_component = sbol2.FunctionalComponent(uri=f\"{re.name}_enzyme\")\n", - " enzyme_component.definition = re\n", - " enzyme_component.displayID = f\"{re.name}_enzyme\"\n", - " enzyme_in_module = False\n", - "\n", - " for comp in assembly_plan.functionalComponents:\n", - " if (comp.displayId == enzyme_component.displayID):\n", - " enzyme_component = comp\n", - " enzyme_in_module = True\n", - "\n", - " if not enzyme_in_module:\n", - " assembly_plan.functionalComponents.add(enzyme_component)\n", - " \n", - " modifier_participation = sbol2.Participation(uri='restriction')\n", - " modifier_participation.participant = enzyme_component\n", - " modifier_participation.roles = ['http://identifiers.org/SBO:0000019']\n", - " participations.append(modifier_participation)\n", - "\n", - " # Inform topology to PyDNA, if not found assuming linear. \n", - " if is_circular(reactant_component_definition):\n", - " circular=True\n", - " linear=False\n", - " else: \n", - " circular=False\n", - " linear=True\n", - " \n", - " reactant_seq = reactant_component_definition.sequences[0]\n", - " reactant_seq = document.getSequence(reactant_seq).elements\n", - " # Dseqrecord is from PyDNA package with reactant sequence\n", - " ds_reactant = Dseqrecord(reactant_seq, circular=circular)\n", - " digested_reactant = ds_reactant.cut(restriction_enzymes_pydna) #TODO see if ds_reactant.cut is working, causing problems downstream\n", - "\n", - "\n", - " if len(digested_reactant)<2 or len(digested_reactant)>3: \n", - " raise NotImplementedError(f'Not supported number of products. Found{len(digested_reactant)}')\n", - " #TODO select them based on content rather than size.\n", - " elif circular and len(digested_reactant)==2:\n", - " part_extract, backbone = sorted(digested_reactant, key=len)\n", - " elif linear and len(digested_reactant)==3:\n", - " prefix, part_extract, suffix = digested_reactant\n", - " else: raise NotImplementedError('The reactant has no valid topology type')\n", - "\n", - " # Compute the length of single strand sticky ends or fusion sites\n", - " product_5_prime_ss_strand, product_5_prime_ss_end = backbone.seq.five_prime_end()\n", - " product_3_prime_ss_strand, product_3_prime_ss_end = backbone.seq.three_prime_end()\n", - " product_sequence = str(backbone.seq)\n", - " prod_backbone_definition, prod_seq = dna_componentdefinition_with_sequence2(identity=f'{reactant_component_definition.displayId}_extracted_backbone', sequence=product_sequence, **kwargs)\n", - " prod_backbone_definition.wasDerivedFrom = reactant_component_definition.identity\n", - " extracts_list.append((prod_backbone_definition, prod_seq))\n", - "\n", - " # five prime overhang\n", - " five_prime_oh_definition = sbol2.ComponentDefinition(uri=f\"{reactant_displayId}_five_prime_oh\") # TODO: ensure circular type is preserved for sbh visualization\n", - " five_prime_oh_definition.addRole('http://identifiers.org/so/SO:0001932')\n", - " five_prime_oh_location = sbol2.Range(uri=\"five_prime_oh_location\", start=1, end=len(product_5_prime_ss_end))\n", - " five_prime_oh_component = sbol2.Component(uri=f\"{reactant_displayId}_five_prime_oh_component\")\n", - " five_prime_oh_component.definition = five_prime_oh_definition\n", - " five_prime_overhang_annotation = sbol2.SequenceAnnotation(uri=\"five_prime_overhang\")\n", - " five_prime_overhang_annotation.locations.add(five_prime_oh_location)\n", - "\n", - " # extracted backbone => point straight to backbone from sbolcanvas\n", - " backbone_location = sbol2.Range(uri=f\"{reactant_displayId}_backbone_location\", start=len(product_5_prime_ss_end)+1, end=len(product_sequence)-len(product_3_prime_ss_end))\n", - " backbone_extract_annotation = sbol2.SequenceAnnotation(uri=f\"{reactant_displayId}_backbone\")\n", - " backbone_extract_annotation.locations.add(backbone_location)\n", - " \n", - " # three prime overhang\n", - " three_prime_oh_definition = sbol2.ComponentDefinition(uri=f\"{reactant_displayId}_three_prime_oh\")\n", - " three_prime_oh_definition.addRole('http://identifiers.org/so/SO:0001933')\n", - " three_prime_oh_location = sbol2.Range(uri=\"three_prime_oh_location\", start=len(product_sequence)-len(product_3_prime_ss_end)+1, end=len(product_sequence)) \n", - " three_prime_oh_component = sbol2.Component(uri=f\"{reactant_displayId}_three_prime_oh_component\")\n", - " three_prime_oh_component.definition = three_prime_oh_definition\n", - " three_prime_overhang_annotation = sbol2.SequenceAnnotation(uri=\"three_prime_overhang\")\n", - " three_prime_overhang_annotation.locations.add(three_prime_oh_location)\n", - "\n", - " anno_component_dict = {}\n", - "\n", - " prod_backbone_definition.components = [five_prime_oh_component, three_prime_oh_component]\n", - " three_prime_overhang_annotation.component = three_prime_oh_component\n", - " five_prime_overhang_annotation.component = five_prime_oh_component\n", - "\n", - " # check these lines\n", - " original_backbone_def_URI = \"\"\n", - "\n", - " #enccode ontologies of overhangs\n", - " for definition in document.componentDefinitions:\n", - " for seqURI in definition.sequences:\n", - " seq = document.getSequence(seqURI)\n", - " if seq.elements.lower() == Seq(product_3_prime_ss_end).reverse_complement():\n", - " three_prime_oh_definition.wasDerivedFrom = definition.identity\n", - " three_prime_sequence = sbol2.Sequence(uri=f\"{three_prime_oh_definition.displayId}_sequence\", elements= seq.elements)\n", - " three_prime_sequence.wasDerivedFrom = seq.identity\n", - " three_prime_oh_definition.sequences = [three_prime_sequence]\n", - " three_prime_oh_definition.types.append(\"http://identifiers.org/SO:0000984\") # single-stranded for overhangs\n", - " \n", - " extracts_list.append((three_prime_oh_definition, three_prime_sequence))\n", - " extracts_list.append((definition, seq)) #add scars to list\n", - "\n", - " elif seq.elements.lower() == product_sequence[4:-4]: \n", - " original_backbone_def_URI = definition.identity\n", - " extracts_list.append((definition, seq))\n", - "\n", - " elif seq.elements.lower() == product_5_prime_ss_end:\n", - " five_prime_oh_definition.wasDerivedFrom = definition.identity\n", - " five_prime_sequence = sbol2.Sequence(uri=f\"{five_prime_oh_definition.displayId}_sequence\", elements= seq.elements)\n", - " five_prime_sequence.wasDerivedFrom = seq.identity\n", - " five_prime_oh_definition.sequences = [five_prime_sequence]\n", - " five_prime_oh_definition.types.append(\"http://identifiers.org/SO:0000984\") # single-stranded for overhangs\n", - "\n", - " extracts_list.append((five_prime_oh_definition, five_prime_sequence))\n", - " extracts_list.append((definition, seq))\n", - "\n", - " #find + add original component to product def & annotation\n", - " for comp in reactant_component_definition.components: \n", - " if comp.definition == original_backbone_def_URI:\n", - " prod_backbone_definition.components.add(comp)\n", - " backbone_extract_annotation.component = comp\n", - "\n", - " prod_backbone_definition.sequenceAnnotations.add(three_prime_overhang_annotation)\n", - " prod_backbone_definition.sequenceAnnotations.add(five_prime_overhang_annotation)\n", - " prod_backbone_definition.sequenceAnnotations.add(backbone_extract_annotation)\n", - " prod_backbone_definition.addRole(tyto.SO.plasmid_vector) \n", - "\n", - " #Add reference to part in backbone\n", - " reactant_component = sbol2.FunctionalComponent(uri=f'{reactant_component_definition.displayId}_backbone_reactant')\n", - " reactant_component.definition = reactant_component_definition\n", - " assembly_plan.functionalComponents.add(reactant_component)\n", - "\n", - " # Create reactant Participation.\n", - " reactant_participation = sbol2.Participation(uri=f'{reactant_component_definition.displayId}_backbone_reactant')\n", - " reactant_participation.participant = reactant_component\n", - " reactant_participation.roles = [sbol2.SBO_REACTANT]\n", - " participations.append(reactant_participation)\n", - " \n", - " prod_component = sbol2.FunctionalComponent(uri=f'{reactant_component_definition.displayId}_backbone_digestion_product')\n", - " prod_component.definition = prod_backbone_definition\n", - " assembly_plan.functionalComponents.add(prod_component)\n", - "\n", - " product_participation = sbol2.Participation(uri=f'{reactant_component_definition.displayId}_backbone_product')\n", - " product_participation.participant = prod_component\n", - " product_participation.roles = [sbol2.SBO_PRODUCT]\n", - " participations.append(product_participation)\n", - " \n", - " # Make Interaction\n", - " interaction = sbol2.Interaction(uri=f\"{reactant_component_definition.displayId}_digestion_interaction\", interaction_type='https://identifiers.org/biomodels.sbo/SBO:0000178')\n", - " interaction.participations = participations\n", - " assembly_plan.interactions.add(interaction)\n", - " \n", - " return extracts_list, assembly_plan" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Tests" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "http://examples.org/ComponentDefinition/UJHDBOTD_extracted_backbone/1\n", - "http://examples.org/Sequence/UJHDBOTD_extracted_backbone_seq/1\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_159_five_prime_oh/1\n", - "http://examples.org/Sequence/UJHDBOTD_159_five_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_F/1\n", - "https://sbolcanvas.org/Scar_F_sequence\n", - "https://sbolcanvas.org/Cir_qxow/1\n", - "https://sbolcanvas.org/Cir_qxow_sequence\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_159_three_prime_oh/1\n", - "http://examples.org/Sequence/UJHDBOTD_159_three_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_A/1\n", - "https://sbolcanvas.org/Scar_A_sequence\n", - "should include role: https://identifiers.org/SO:0000755\n" - ] - }, - { - "data": { - "text/plain": [ - "'Valid.'" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "bb_doc = sbol2.Document()\n", - "bb_doc.read('tests/test_files/backbone.xml')\n", - "\n", - "md = bb_doc.getModuleDefinition(\"https://sbolcanvas.org/module1\")\n", - "assembly_plan = sbol2.ModuleDefinition('bb_assembly_plan')\n", - "\n", - "extracts_tuple_list, assembly_plan = backbone_digestion(md, [bsai], assembly_plan, bb_doc)\n", - "part_extract, seq = extracts_tuple_list[0]\n", - "\n", - "new_doc = sbol2.Document()\n", - "for extract, sequence in extracts_tuple_list:\n", - " print(extract.identity)\n", - " print(sequence.identity)\n", - " new_doc.add(extract)\n", - " new_doc.add(sequence)\n", - "new_doc.add(assembly_plan)\n", - "print(f\"should include role: {tyto.SO.plasmid_vector}\")\n", - "new_doc.write('new_bb_test.xml')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Ligation" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Target Function (do not run)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "#target function\n", - "def ligation(reactants:List[sbol3.Component], assembly_plan:sbol3.Component)-> List[Tuple[sbol3.Component, sbol3.Sequence]]:\n", - " \"\"\"Ligates Components using base complementarity and creates a product Component and a ligation Interaction.\n", - "\n", - " :param reactant: DNA to be ligated as SBOL Component. \n", - " :return: A tuple of Component and Interaction.\n", - " \"\"\"\n", - " # Create a dictionary that maps each first and last 4 letters to a list of strings that have those letters.\n", - " reactant_parts = []\n", - " fusion_sites_set = set()\n", - " for reactant in reactants:\n", - " fusion_site_3prime_length = reactant.features[0].locations[0].end - reactant.features[0].locations[0].start\n", - " fusion_site_5prime_length = reactant.features[0].locations[1].end - reactant.features[0].locations[1].start\n", - " if fusion_site_3prime_length == fusion_site_5prime_length:\n", - " fusion_site_length = fusion_site_3prime_length + 1 # if the fusion site is 4 bp long, the start will be 1 and end 4, 4-1 = 3, so we add 1 to get 4.\n", - " fusion_sites_set.add(fusion_site_length)\n", - " if len(fusion_sites_set) > 1:\n", - " raise ValueError(f'Fusion sites of different length within different parts. Check {reactant.identity} ')\n", - " else:\n", - " raise ValueError(f'Fusion sites of different length within the same part. Check {reactant.identity}')\n", - " if tyto.SO.plasmid_vector in reactant.roles:\n", - " reactant_parts.append(reactant)\n", - " elif tyto.SO.engineered_insert in reactant.roles:\n", - " reactant_parts.append(reactant)\n", - " else:\n", - " raise ValueError(f'Part {reactant.identity} does not have a valid role')\n", - " # remove the backbones if any from the reactants, to create the composite\n", - " groups = {}\n", - " for reactant in reactant_parts:\n", - " first_four_letters = reactant.sequences[0].lookup().elements[:fusion_site_length].lower()\n", - " last_four_letters = reactant.sequences[0].lookup().elements[-fusion_site_length:].lower()\n", - " part_syntax = f'{first_four_letters}_{last_four_letters}'\n", - " if part_syntax not in groups:\n", - " groups[part_syntax] = []\n", - " groups[part_syntax].append(reactant)\n", - " else: groups[part_syntax].append(reactant)\n", - " # groups is a dictionary of lists of parts that have the same first and last 4 letters\n", - " # list_of_combinations_per_assembly is a list of tuples of parts that can be ligated together\n", - " list_of_parts_per_combination = list(product(*groups.values())) #cartesian product\n", - " # create list_of_composites_per_assembly from list_of_combinations_per_assembly\n", - " list_of_composites_per_assembly = []\n", - " for combination in list_of_parts_per_combination:\n", - " list_of_parts_per_composite = [combination[0]] \n", - " insert_sequence = combination[0].sequences[0].lookup().elements\n", - " remaining_parts = list(combination[1:])\n", - " it = 1\n", - " while remaining_parts: \n", - " remaining_parts_before = len(remaining_parts) \n", - " for part in remaining_parts:\n", - " # match insert sequence 5' to part 3'\n", - " if part.sequences[0].lookup().elements[:fusion_site_length].lower() == insert_sequence[-fusion_site_length:].lower():\n", - " insert_sequence = insert_sequence[:-fusion_site_length] + part.sequences[0].lookup().elements\n", - " list_of_parts_per_composite.append(part)\n", - " remaining_parts.remove(part)\n", - " # match insert sequence 3' to part 5'\n", - " elif part.sequences[0].lookup().elements[-fusion_site_length:].lower() == insert_sequence[:fusion_site_length].lower():\n", - " insert_sequence = part.sequences[0].lookup().elements + insert_sequence[fusion_site_length:]\n", - " list_of_parts_per_composite.insert(0, part)\n", - " remaining_parts.remove(part)\n", - " remaining_parts_after = len(remaining_parts)\n", - " \n", - " if remaining_parts_before == remaining_parts_after:\n", - " it += 1\n", - " if it > 5: #5 was chosen arbitrarily to avoid infinite loops\n", - " print(groups)\n", - " raise ValueError('No match found, check the parts and their fusion sites')\n", - " list_of_composites_per_assembly.append(list_of_parts_per_composite)\n", - "\n", - " # transform list_of_parts_per_assembly into list of composites\n", - " products_list = []\n", - " participations = []\n", - " composite_number = 1\n", - " for composite in list_of_composites_per_assembly: # a composite of the form [A,B,C]\n", - " # calculate sequence\n", - " composite_sequence_str = \"\"\n", - " composite_name = \"\"\n", - " #part_subcomponents = []\n", - " part_extract_subcomponents = []\n", - " for part_extract in composite:\n", - " composite_sequence_str = composite_sequence_str + part_extract.sequences[0].lookup().elements[:-fusion_site_length] #needs a version for linear\n", - " # create participations\n", - " part_extract_subcomponent = sbol3.SubComponent(part_extract) # LocalSubComponent??\n", - " part_extract_subcomponents.append(part_extract_subcomponent)\n", - " composite_name = composite_name +'_'+ part_extract.name\n", - " # create dna componente and sequence\n", - " composite_component, composite_seq = dna_component_with_sequence(f'composite_{composite_number}{composite_name}', composite_sequence_str) # **kwarads use in future?\n", - " composite_component.name = f'composite_{composite_number}{composite_name}'\n", - " composite_component.roles.append(sbol3.SO_ENGINEERED_REGION)\n", - " composite_component.features = part_extract_subcomponents\n", - " for i in range(len(composite_component.features )-1):\n", - " composite_component.constraints = [sbol3.Constraint(restriction='http://sbols.org/v3#meets', subject=composite_component.features[i], object=composite_component.features[i+1])]\n", - " products_list.append([composite_component, composite_seq])\n", - " composite_number += 1\n", - " return products_list" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Alphabetical Suffix Helper Function (for scars)" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [], - "source": [ - "def number_to_suffix(n):\n", - " suffix = \"\"\n", - " while n > 0:\n", - " n -= 1 # Adjust to make A = 0, B = 1, ..., Z = 25\n", - " remainder = n % 26 # Get the current character\n", - " suffix = chr(ord('A') + remainder) + suffix # Build the suffix\n", - " n = n // 26 # Move to the next higher place value\n", - " return suffix\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## SBOL2 implementation" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def ligation2(reactants:List[sbol2.ComponentDefinition], assembly_plan: sbol2.ModuleDefinition, document: sbol2.Document, ligase: sbol2.ComponentDefinition=None)->List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]]:\n", - " \"\"\"Ligates Components using base complementarity and creates a product Component and a ligation Interaction.\n", - "\n", - " :param reactants: DNA parts to be ligated as SBOL ModuleDefinition. \n", - " :param assembly_plan: SBOL ModuleDefinition to contain the functional components, interactions, and participants\n", - " :param document: SBOL2 document containing all reactant componentdefinitions.\n", - " :return: A tuple of ComponentDefinition and Sequence.\n", - " :param ligase: as SBOL ComponentDefinition\n", - " \"\"\"\n", - " if ligase == None:\n", - " ligase = sbol2.ComponentDefinition(uri=\"T4_Ligase\")\n", - " ligase.name = \"T4_Ligase\"\n", - " ligase.types = sbol2.BIOPAX_PROTEIN\n", - " document.add(ligase)\n", - "\n", - " ligase_component = sbol2.FunctionalComponent(uri=\"T4_Ligase\")\n", - " ligase_component.definition = ligase\n", - " assembly_plan.functionalComponents.add(ligase_component)\n", - "\n", - " modifier_participation = sbol2.Participation(uri='ligation')\n", - " modifier_participation.participant = ligase_component\n", - " modifier_participation.roles = ['http://identifiers.org/biomodels.sbo/SBO:0000019']\n", - "\n", - " # Create a dictionary that maps each first and last 4 letters to a list of strings that have those letters.\n", - " reactant_parts = []\n", - " fusion_sites_set = set()\n", - " for reactant in reactants:\n", - " fusion_site_3prime_length = reactant.sequenceAnnotations[0].locations[0].end - reactant.sequenceAnnotations[0].locations[0].start\n", - " fusion_site_5prime_length = reactant.sequenceAnnotations[1].locations[0].end - reactant.sequenceAnnotations[1].locations[0].start\n", - " if fusion_site_3prime_length == fusion_site_5prime_length:\n", - " fusion_site_length = fusion_site_3prime_length + 1 # if the fusion site is 4 bp long, the start will be 1 and end 4, 4-1 = 3, so we add 1 to get 4.\n", - " fusion_sites_set.add(fusion_site_length)\n", - " if len(fusion_sites_set) > 1:\n", - " raise ValueError(f'Fusion sites of different length within different parts. Check {reactant.identity} ')\n", - " else:\n", - " raise ValueError(f'Fusion sites of different length within the same part. Check {reactant.identity}')\n", - " if tyto.SO.plasmid_vector in reactant.roles:\n", - " reactant_parts.append(reactant)\n", - " elif tyto.SO.engineered_insert in reactant.roles:\n", - " reactant_parts.append(reactant)\n", - " else:\n", - " raise ValueError(f'Part {reactant.identity} does not have a valid role')\n", - " \n", - " # remove the backbones if any from the reactants, to create the composite\n", - " groups = {}\n", - " for reactant in reactant_parts:\n", - " reactant_seq = reactant.sequences[0]\n", - " first_four_letters = document.getSequence(reactant_seq).elements[:fusion_site_length].lower()\n", - " last_four_letters = document.getSequence(reactant_seq).elements[-fusion_site_length:].lower()\n", - " part_syntax = f'{first_four_letters}_{last_four_letters}'\n", - " if part_syntax not in groups:\n", - " groups[part_syntax] = []\n", - " groups[part_syntax].append(reactant)\n", - " else: groups[part_syntax].append(reactant)\n", - " # groups is a dictionary of lists of parts that have the same first and last 4 letters\n", - " # list_of_combinations_per_assembly is a list of tuples of parts that can be ligated together\n", - " list_of_parts_per_combination = list(product(*groups.values())) #cartesian product\n", - " # create list_of_composites_per_assembly from list_of_combinations_per_assembly\n", - " list_of_composites_per_assembly = []\n", - " for combination in list_of_parts_per_combination:\n", - " list_of_parts_per_composite = [combination[0]] \n", - " insert_sequence_uri = combination[0].sequences[0]\n", - " insert_sequence = document.getSequence(insert_sequence_uri).elements\n", - " remaining_parts = list(combination[1:])\n", - " it = 1\n", - " while remaining_parts: \n", - " remaining_parts_before = len(remaining_parts) \n", - " for part in remaining_parts:\n", - " # match insert sequence 5' to part 3'\n", - " part_sequence_uri = part.sequences[0]\n", - " if document.getSequence(part_sequence_uri).elements[:fusion_site_length].lower() == insert_sequence[-fusion_site_length:].lower():\n", - " insert_sequence = insert_sequence[:-fusion_site_length] + document.getSequence(part_sequence_uri).elements\n", - " list_of_parts_per_composite.append(part) #add sequence annotation here, index based on insert_sequence\n", - " remaining_parts.remove(part)\n", - " # match insert sequence 3' to part 5'\n", - " elif document.getSequence(part_sequence_uri).elements[-fusion_site_length:].lower() == insert_sequence[:fusion_site_length].lower():\n", - " insert_sequence = document.getSequence(part_sequence_uri).elements + insert_sequence[fusion_site_length:]\n", - " list_of_parts_per_composite.insert(0, part)\n", - " remaining_parts.remove(part)\n", - " remaining_parts_after = len(remaining_parts)\n", - " \n", - " if remaining_parts_before == remaining_parts_after:\n", - " it += 1\n", - " if it > 5: #5 was chosen arbitrarily to avoid infinite loops\n", - " print(groups)\n", - " raise ValueError('No match found, check the parts and their fusion sites')\n", - " list_of_composites_per_assembly.append(list_of_parts_per_composite)\n", - "\n", - " # transform list_of_parts_per_assembly into list of composites\n", - " products_list = []\n", - " participations = []\n", - " composite_number = 1\n", - " participations.append(modifier_participation)\n", - "\n", - " # TODO: use componentinstances to append \"subcomponents\" to each definition that is a composite component. all composites share the \"subcomponents\"\n", - " for composite in list_of_composites_per_assembly: # a composite of the form [A,B,C]\n", - " # calculate sequence\n", - " composite_sequence_str = \"\"\n", - " composite_name = \"\"\n", - " prev_three_prime = composite[len(composite)-1].components[1].definition # componentdefinitionuri\n", - " prev_three_prime_definition = document.getComponentDefinition(prev_three_prime)\n", - " scar_index = 1\n", - " anno_list = []\n", - "\n", - " part_extract_definitions = []\n", - " for part_extract in composite:\n", - " part_extract_sequence_uri = part_extract.sequences[0]\n", - " part_extract_sequence = document.getSequence(part_extract_sequence_uri).elements\n", - " temp_extract_components = []\n", - " # TODO create participations\n", - " reactant_component = sbol2.FunctionalComponent(uri=f\"{part_extract.displayId}_reactant\")\n", - " reactant_component.definition = part_extract # TODO do not make new components, instead derive product functionalcomponents from the assembly_plan moduledefinition to add to the ligation interaction/participation\n", - " for fc in assembly_plan.functionalComponents:\n", - " if fc.definition == reactant_component.definition:\n", - " print(f\"match: {fc.displayId}, {reactant_component.displayId}\")\n", - " reactant_component = fc\n", - "\n", - " reactant_participation = sbol2.Participation(uri=f'{part_extract.displayId}_ligation')\n", - " reactant_participation.participant = reactant_component\n", - " reactant_participation.roles = [sbol2.SBO_REACTANT]\n", - " participations.append(reactant_participation)\n", - "\n", - " for comp in part_extract.components:\n", - " if \"http://identifiers.org/so/SO:0001932\" in document.getComponentDefinition(comp.definition).roles: #five prime\n", - " scar_definition = sbol2.ComponentDefinition(uri=f\"Scar_{number_to_suffix(scar_index)}\")\n", - " scar_sequence = sbol2.Sequence(uri=f\"Scar_{number_to_suffix(scar_index)}_sequence\", elements=document.getSequence(prev_three_prime_definition.sequences[0]).elements)\n", - " scar_definition.sequences = [scar_sequence]\n", - " scar_definition.wasDerivedFrom = [comp.definition, prev_three_prime]\n", - " scar_definition.roles = [\"http://identifiers.org/so/SO:0001953\"]\n", - " temp_extract_components.append(scar_definition.identity)\n", - " \n", - " document.add(scar_definition)\n", - " document.add(scar_sequence)\n", - "\n", - " scar_location = sbol2.Range(uri=f\"Scar_{number_to_suffix(scar_index)}_location\", start= len(composite_sequence_str)+1, end=len(composite_sequence_str)+fusion_site_length)\n", - " scar_anno = sbol2.SequenceAnnotation(uri=f\"Scar_{number_to_suffix(scar_index)}_annotation\")\n", - " scar_anno.locations.add(scar_location)\n", - " anno_list.append(scar_anno)\n", - " scar_index += 1\n", - " elif \"http://identifiers.org/so/SO:0001933\" in document.getComponentDefinition(comp.definition).roles: #three prime\n", - " prev_three_prime = comp.definition\n", - " prev_three_prime_definition = document.getComponentDefinition(prev_three_prime)\n", - " else:\n", - " temp_extract_components.append(comp.definition)\n", - " comp_location = sbol2.Range(uri=f\"{comp.displayId}_location\", start= len(composite_sequence_str)+fusion_site_length+1, end=len(composite_sequence_str)+len(part_extract_sequence[:-4])) #TODO check if seq len is correct\n", - " comp_anno = sbol2.SequenceAnnotation(uri=f\"{comp.displayId}_annotation\")\n", - " comp_anno.locations.add(comp_location)\n", - " anno_list.append(comp_anno)\n", - "\n", - " part_extract_definitions.extend(temp_extract_components)\n", - "\n", - " composite_sequence_str = composite_sequence_str + part_extract_sequence[:-fusion_site_length] #needs a version for linear\n", - " # composite_name = composite_name + '_' + re.match(r'^[^_]*_[^_]*', part_extract.displayId).group()\n", - "\n", - " for anno in anno_list:\n", - " for location in anno.locations:\n", - " print(anno.identity, location.start, location.end)\n", - " \n", - "\n", - " # create dna component and sequence\n", - " composite_component_definition, composite_seq = dna_componentdefinition_with_sequence2(f'composite_{composite_number}', composite_sequence_str)\n", - " composite_component_definition.name = f'composite_{composite_number}'\n", - " composite_component_definition.addRole('http://identifiers.org/SO:0000804') #engineered region\n", - " composite_component_definition.addType('http://identifiers.org/so/SO:0000988')\n", - "\n", - " part_extract_components = [] \n", - " \n", - "\n", - " for i, definition in enumerate(part_extract_definitions):\n", - " def_object = document.getComponentDefinition(definition)\n", - " comp = sbol2.Component(uri=def_object.displayId)\n", - " comp.definition = definition\n", - " composite_component_definition.components.add(comp)\n", - " \n", - " anno_list[i].component = comp \n", - "\n", - " composite_component_definition.sequenceAnnotations = anno_list\n", - "\n", - " prod_functional_component = sbol2.FunctionalComponent(uri=f\"{composite_component_definition.name}\")\n", - " prod_functional_component.definition = composite_component_definition\n", - " assembly_plan.functionalComponents.add(prod_functional_component)\n", - "\n", - " product_participation = sbol2.Participation(uri=f\"{composite_component_definition.name}_product\")\n", - " product_participation.participant = prod_functional_component\n", - " product_participation.roles = [sbol2.SBO_PRODUCT]\n", - " participations.append(product_participation)\n", - " \n", - " # Make Interaction\n", - " interaction = sbol2.Interaction(uri=f\"{composite_component_definition.name}_ligation_interaction\", interaction_type='http://identifiers.org/biomodels.sbo/SBO:0000695')\n", - " interaction.participations = participations\n", - " assembly_plan.interactions.add(interaction)\n", - "\n", - " products_list.append([composite_component_definition, composite_seq])\n", - " composite_number += 1\n", - " return products_list" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Tests" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "reacant 1: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_70_extracted_part/1\n", - "reacant 1: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_70_three_prime_oh/1\n", - "reacant 1: adding part https://sbolcanvas.org/Scar_B/1\n", - "reacant 1: adding part https://sbolcanvas.org/J23101/1\n", - "reacant 1: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_70_five_prime_oh/1\n", - "reacant 1: adding part https://sbolcanvas.org/Scar_A/1\n", - "reacant 2: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_45_extracted_part/1\n", - "reacant 2: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_45_five_prime_oh/1\n", - "(, 'Cannot add https://sbolcanvas.org/Scar_B/1 to Document. An object with this identity is already contained in the Document')\n", - "reacant 2: adding part https://sbolcanvas.org/Scar_B/1\n", - "reacant 2: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_45_three_prime_oh/1\n", - "reacant 2: adding part https://sbolcanvas.org/Scar_C/1\n", - "reacant 2: adding part https://sbolcanvas.org/B0034/1\n", - "reacant 3: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_106_extracted_part/1\n", - "reacant 3: adding part https://sbolcanvas.org/GFP/1\n", - "reacant 3: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_106_five_prime_oh/1\n", - "(, 'Cannot add https://sbolcanvas.org/Scar_C/1 to Document. An object with this identity is already contained in the Document')\n", - "reacant 3: adding part https://sbolcanvas.org/Scar_C/1\n", - "reacant 3: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_106_three_prime_oh/1\n", - "reacant 3: adding part https://sbolcanvas.org/Scar_D/1\n", - "reacant 4: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_134_extracted_part/1\n", - "reacant 4: adding part https://sbolcanvas.org/B0015/1\n", - "reacant 4: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_134_three_prime_oh/1\n", - "reacant 4: adding part https://sbolcanvas.org/Scar_F/1\n", - "reacant 4: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_134_five_prime_oh/1\n", - "(, 'Cannot add https://sbolcanvas.org/Scar_D/1 to Document. An object with this identity is already contained in the Document')\n", - "reacant 4: adding part https://sbolcanvas.org/Scar_D/1\n", - "backbone: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_extracted_backbone/1\n", - "backbone: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_159_five_prime_oh/1\n", - "(, 'Cannot add https://sbolcanvas.org/Scar_F/1 to Document. An object with this identity is already contained in the Document')\n", - "backbone: adding part https://sbolcanvas.org/Scar_F/1\n", - "backbone: adding part https://sbolcanvas.org/Cir_qxow/1\n", - "backbone: adding part https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_159_three_prime_oh/1\n", - "(, 'Cannot add https://sbolcanvas.org/Scar_A/1 to Document. An object with this identity is already contained in the Document')\n", - "backbone: adding part https://sbolcanvas.org/Scar_A/1\n" - ] - }, - { - "data": { - "text/plain": [ - "'Valid.'" - ] - }, - "execution_count": 44, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ligation_doc = sbol2.Document()\n", - "temp_doc = sbol2.Document()\n", - "reactants_list = []\n", - "assembly_plan = sbol2.ModuleDefinition('assembly_plan')\n", - "parts = ['tests/test_files/pro_in_bb.xml','tests/test_files/rbs_in_bb.xml', 'tests/test_files/cds_in_bb.xml', 'tests/test_files/terminator_in_bb.xml']\n", - "\n", - "for i, part in enumerate(parts):\n", - " temp_doc.read(part)\n", - " md = temp_doc.getModuleDefinition('https://sbolcanvas.org/module1')\n", - " extracts_tuple_list, assembly_plan = part_digestion(md, [bsai], assembly_plan, temp_doc)\n", - "\n", - " for extract, sequence in extracts_tuple_list:\n", - " try:\n", - " ligation_doc.add(extract)\n", - " ligation_doc.add(sequence)\n", - " except Exception as e: print(e)\n", - " print(f\"reacant {i+1}: adding part {extract}\")\n", - "\n", - " reactants_list.append(extracts_tuple_list[0][0])\n", - "\n", - "temp_doc.read('tests/test_files/backbone.xml')\n", - "# run digestion, extract component + sequence, add to ligation_doc, reactants_list\n", - "md = temp_doc.getModuleDefinition('https://sbolcanvas.org/module1')\n", - "extracts_tuple_list, assembly_plan = backbone_digestion(md, [bsai], assembly_plan, temp_doc)\n", - "for extract, seq in extracts_tuple_list:\n", - " try:\n", - " ligation_doc.add(extract)\n", - " ligation_doc.add(seq)\n", - " except Exception as e: print(e)\n", - " print(f\"backbone: adding part {extract}\")\n", - "\n", - "ligation_doc.add(assembly_plan)\n", - "reactants_list.append(extracts_tuple_list[0][0])\n", - "\n", - " \n", - "ligation_doc.add(bsai)\n", - "ligation_doc.write('ligation_test1.xml')" - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_70_extracted_part/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_70_extracted_part_seq/1\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_70_three_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_70_three_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_B/1\n", - "https://sbolcanvas.org/Scar_B_sequence\n", - "https://sbolcanvas.org/J23101/1\n", - "https://sbolcanvas.org/J23101_sequence\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_70_five_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_70_five_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_A/1\n", - "https://sbolcanvas.org/Scar_A_sequence\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_45_extracted_part/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_45_extracted_part_seq/1\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_45_five_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_45_five_prime_oh_sequence/1\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_45_three_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_45_three_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_C/1\n", - "https://sbolcanvas.org/Scar_C_sequence\n", - "https://sbolcanvas.org/B0034/1\n", - "https://sbolcanvas.org/B0034_sequence\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_106_extracted_part/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_106_extracted_part_seq/1\n", - "https://sbolcanvas.org/GFP/1\n", - "https://sbolcanvas.org/GFP_sequence\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_106_five_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_106_five_prime_oh_sequence/1\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_106_three_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_106_three_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_D/1\n", - "https://sbolcanvas.org/Scar_D_sequence\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_134_extracted_part/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_134_extracted_part_seq/1\n", - "https://sbolcanvas.org/B0015/1\n", - "https://sbolcanvas.org/B0015_sequence\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_134_three_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_134_three_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Scar_F/1\n", - "https://sbolcanvas.org/Scar_F_sequence\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_134_five_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_134_five_prime_oh_sequence/1\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_extracted_backbone/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_extracted_backbone_seq/1\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_159_five_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_159_five_prime_oh_sequence/1\n", - "https://sbolcanvas.org/Cir_qxow/1\n", - "https://sbolcanvas.org/Cir_qxow_sequence\n", - "https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_159_three_prime_oh/1\n", - "https://SBOL2Build.org/Sequence/UJHDBOTD_159_three_prime_oh_sequence/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/1\n", - "http://examples.org/ComponentDefinition/BsaI/1\n", - "https://SBOL2Build.org/ComponentDefinition/T4_Ligase/1\n", - "https://SBOL2Build.org/ComponentDefinition/Scar_A/1\n", - "https://SBOL2Build.org/Sequence/Scar_A_sequence/1\n", - "https://SBOL2Build.org/ComponentDefinition/Scar_B/1\n", - "https://SBOL2Build.org/Sequence/Scar_B_sequence/1\n", - "https://SBOL2Build.org/ComponentDefinition/Scar_C/1\n", - "https://SBOL2Build.org/Sequence/Scar_C_sequence/1\n", - "https://SBOL2Build.org/ComponentDefinition/Scar_D/1\n", - "https://SBOL2Build.org/Sequence/Scar_D_sequence/1\n", - "https://SBOL2Build.org/ComponentDefinition/Scar_E/1\n", - "https://SBOL2Build.org/Sequence/Scar_E_sequence/1\n", - "https://SBOL2Build.org/ComponentDefinition/composite_1/1\n", - "https://SBOL2Build.org/Sequence/composite_1_seq/1\n", - "\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/BsaI_enzyme/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_70_reactant/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_70_digestion_product/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_45_reactant/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_45_digestion_product/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_106_reactant/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_106_digestion_product/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_134_reactant/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_134_digestion_product/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_backbone_reactant/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/UJHDBOTD_backbone_digestion_product/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/T4_Ligase/1\n", - "https://SBOL2Build.org/ModuleDefinition/assembly_plan/composite_1/1\n" - ] - } - ], - "source": [ - "for obj in ligation_doc:\n", - " print(obj)\n", - "\n", - "obj = ligation_doc.find('https://SBOL2Build.org/ModuleDefinition/assembly_plan/1')\n", - "print(type(obj))\n", - "for comp in obj.functionalComponents:\n", - " print(comp.identity)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Ligation Test" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "http://examples.org/ComponentDefinition/UJHDBOTD_70_extracted_part/three_prime_overhang/1 40 43\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_70_extracted_part/five_prime_overhang/1 1 4\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_70_extracted_part/UJHDBOTD_70_part/1 5 39\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_45_extracted_part/three_prime_overhang/1 26 29\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_45_extracted_part/five_prime_overhang/1 1 4\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_45_extracted_part/UJHDBOTD_45_part/1 5 25\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_106_extracted_part/three_prime_overhang/1 722 725\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_106_extracted_part/five_prime_overhang/1 1 4\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_106_extracted_part/UJHDBOTD_106_part/1 5 721\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_134_extracted_part/three_prime_overhang/1 134 137\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_134_extracted_part/five_prime_overhang/1 1 4\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_134_extracted_part/UJHDBOTD_134_part/1 5 133\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_extracted_backbone/three_prime_overhang/1 2200 2203\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_extracted_backbone/five_prime_overhang/1 1 4\n", - "http://examples.org/ComponentDefinition/UJHDBOTD_extracted_backbone/UJHDBOTD_159_backbone/1 5 2199\n" - ] - } - ], - "source": [ - "for definition in reactants_list:\n", - " for anno in definition.sequenceAnnotations:\n", - " for location in anno.locations:\n", - " print(anno.identity, location.start, location.end)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### WRITE TEST" - ] - }, - { - "cell_type": "code", - "execution_count": 46, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "reactant: https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_70_extracted_part/J23101_3/1\n", - "reactant: https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_45_extracted_part/B0034_3/1\n", - "reactant: https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_106_extracted_part/GFP_3/1\n", - "reactant: https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_134_extracted_part/B0015_3/1\n", - "reactant: https://SBOL2Build.org/ComponentDefinition/UJHDBOTD_extracted_backbone/Cir_qxow_5/1\n", - "match: UJHDBOTD_backbone_digestion_product, UJHDBOTD_extracted_backbone_reactant\n", - "match: UJHDBOTD_70_digestion_product, UJHDBOTD_70_extracted_part_reactant\n", - "match: UJHDBOTD_45_digestion_product, UJHDBOTD_45_extracted_part_reactant\n", - "match: UJHDBOTD_106_digestion_product, UJHDBOTD_106_extracted_part_reactant\n", - "match: UJHDBOTD_134_digestion_product, UJHDBOTD_134_extracted_part_reactant\n", - "https://SBOL2Build.org/SequenceAnnotation/Scar_A_annotation/1 1 4\n", - "https://SBOL2Build.org/SequenceAnnotation/Cir_qxow_5_annotation/1 5 2199\n", - "https://SBOL2Build.org/SequenceAnnotation/Scar_B_annotation/1 2200 2203\n", - "https://SBOL2Build.org/SequenceAnnotation/J23101_3_annotation/1 2204 2238\n", - "https://SBOL2Build.org/SequenceAnnotation/Scar_C_annotation/1 2239 2242\n", - "https://SBOL2Build.org/SequenceAnnotation/B0034_3_annotation/1 2243 2263\n", - "https://SBOL2Build.org/SequenceAnnotation/Scar_D_annotation/1 2264 2267\n", - "https://SBOL2Build.org/SequenceAnnotation/GFP_3_annotation/1 2268 2984\n", - "https://SBOL2Build.org/SequenceAnnotation/Scar_E_annotation/1 2985 2988\n", - "https://SBOL2Build.org/SequenceAnnotation/B0015_3_annotation/1 2989 3117\n", - "https://SBOL2Build.org/ComponentDefinition/composite_1/1\n", - "https://SBOL2Build.org/Sequence/composite_1_seq/1\n", - "https://SBOL2Build.org\n" - ] - }, - { - "data": { - "text/plain": [ - "'Valid.'" - ] - }, - "execution_count": 46, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "for reactant in reactants_list:\n", - " print(f\"reactant: {reactant.components[2]}\")\n", - "pl = ligation2(reactants_list, assembly_plan, ligation_doc)\n", - "\n", - "for l in pl:\n", - " for obj in l:\n", - " print(obj.identity)\n", - " ligation_doc.add(obj)\n", - "\n", - "print(sbol2.Config.getHomespace()) \n", - "ligation_doc.write('ligation_test_forreal.xml')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_70_digestion_interaction/restriction/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/BsaI_enzyme/1\n", - "definition = http://examples.org/ComponentDefinition/BsaI/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_70_digestion_interaction/UJHDBOTD_70_reactant/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_70_reactant/1\n", - "definition = https://sbolcanvas.org/UJHDBOTD/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_70_digestion_interaction/UJHDBOTD_70_product/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_70_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_70_extracted_part/1\n", - "\n", - "\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_45_digestion_interaction/restriction/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/BsaI_enzyme/1\n", - "definition = http://examples.org/ComponentDefinition/BsaI/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_45_digestion_interaction/UJHDBOTD_45_reactant/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_45_reactant/1\n", - "definition = https://sbolcanvas.org/UJHDBOTD/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_45_digestion_interaction/UJHDBOTD_45_product/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_45_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_45_extracted_part/1\n", - "\n", - "\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_106_digestion_interaction/restriction/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/BsaI_enzyme/1\n", - "definition = http://examples.org/ComponentDefinition/BsaI/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_106_digestion_interaction/UJHDBOTD_106_reactant/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_106_reactant/1\n", - "definition = https://sbolcanvas.org/UJHDBOTD/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_106_digestion_interaction/UJHDBOTD_106_product/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_106_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_106_extracted_part/1\n", - "\n", - "\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_134_digestion_interaction/restriction/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/BsaI_enzyme/1\n", - "definition = http://examples.org/ComponentDefinition/BsaI/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_134_digestion_interaction/UJHDBOTD_134_reactant/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_134_reactant/1\n", - "definition = https://sbolcanvas.org/UJHDBOTD/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_134_digestion_interaction/UJHDBOTD_134_product/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_134_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_134_extracted_part/1\n", - "\n", - "\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_digestion_interaction/restriction/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/BsaI_enzyme/1\n", - "definition = http://examples.org/ComponentDefinition/BsaI/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_digestion_interaction/UJHDBOTD_backbone_reactant/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_backbone_reactant/1\n", - "definition = https://sbolcanvas.org/UJHDBOTD/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_digestion_interaction/UJHDBOTD_backbone_product/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_backbone_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_extracted_backbone/1\n", - "\n", - "\n", - "http://examples.org/ModuleDefinition/assembly_plan/composite_1_ligation_interaction/ligation/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/T4_Ligase/1\n", - "definition = http://examples.org/ComponentDefinition/T4_Ligase/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/composite_1_ligation_interaction/UJHDBOTD_extracted_backbone_ligation/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_backbone_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_extracted_backbone/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/composite_1_ligation_interaction/UJHDBOTD_70_extracted_part_ligation/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_70_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_70_extracted_part/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/composite_1_ligation_interaction/UJHDBOTD_45_extracted_part_ligation/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_45_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_45_extracted_part/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/composite_1_ligation_interaction/UJHDBOTD_106_extracted_part_ligation/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_106_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_106_extracted_part/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/composite_1_ligation_interaction/UJHDBOTD_134_extracted_part_ligation/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/UJHDBOTD_134_digestion_product/1\n", - "definition = http://examples.org/ComponentDefinition/UJHDBOTD_134_extracted_part/1\n", - "http://examples.org/ModuleDefinition/assembly_plan/composite_1_ligation_interaction/composite_1_product/1\n", - "participant = http://examples.org/ModuleDefinition/assembly_plan/composite_1/1\n", - "definition = http://examples.org/ComponentDefinition/composite_1/1\n", - "\n", - "\n" - ] - } - ], - "source": [ - "for interaction in ligation_doc.moduleDefinitions[0].interactions:\n", - " for participation in interaction.participations:\n", - " print(participation)\n", - " print(\"participant = \", participation.participant)\n", - " print(\"definition = \", ligation_doc.find(participation.participant).definition)\n", - " print(\"\\n\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Valid.'" - ] - }, - "execution_count": 34, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ligation_doc.moduleDefinitions[0].name = \"test\"\n", - "ligation_doc.moduleDefinitions[0].name\n", - "\n", - "ligation_doc.write(\"name_test.xml\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "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.12.7" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} From 1ad96f2e87f6a4ff6ee13dadcf2d7ad2f05c1e68 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Mon, 6 Apr 2026 13:22:59 -0600 Subject: [PATCH 11/93] backbone now listed last in product --- src/buildcompiler/sbol2build.py | 36 +++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/buildcompiler/sbol2build.py b/src/buildcompiler/sbol2build.py index 4298c18..06958de 100644 --- a/src/buildcompiler/sbol2build.py +++ b/src/buildcompiler/sbol2build.py @@ -857,6 +857,7 @@ def ligation( insert_sequence_uri = combination[0].sequences[0] insert_sequence = source_document.getSequence(insert_sequence_uri).elements remaining_parts = list(combination[1:]) + insert_3prime_match_id = None it = 1 while remaining_parts: remaining_parts_before = len(remaining_parts) @@ -869,27 +870,31 @@ def ligation( .lower() == insert_sequence[-fusion_site_length:].lower() ): - insert_sequence = ( - insert_sequence[:-fusion_site_length] - + source_document.getSequence(part_sequence_uri).elements - ) - list_of_parts_per_composite.append( - part - ) # add sequence annotation here, index based on insert_sequence - remaining_parts.remove(part) - # match insert sequence 3' to part 5' + if ( + len(remaining_parts) == 1 + and part.identity == insert_3prime_match_id + ): # check flag and match backbone 5' on final part 3' + insert_sequence = ( + insert_sequence[:-fusion_site_length] + + source_document.getSequence(part_sequence_uri).elements + ) + list_of_parts_per_composite.append(part) + remaining_parts.remove(part) + elif len(remaining_parts) > 1: + insert_sequence = ( + insert_sequence[:-fusion_site_length] + + source_document.getSequence(part_sequence_uri).elements + ) + list_of_parts_per_composite.append(part) + remaining_parts.remove(part) + # match backbone 5' to insert sequence 3', set flag elif ( source_document.getSequence(part_sequence_uri) .elements[-fusion_site_length:] .lower() == insert_sequence[:fusion_site_length].lower() ): - insert_sequence = ( - source_document.getSequence(part_sequence_uri).elements - + insert_sequence[fusion_site_length:] - ) - list_of_parts_per_composite.insert(0, part) - remaining_parts.remove(part) + insert_3prime_match_id = part.identity remaining_parts_after = len(remaining_parts) if remaining_parts_before == remaining_parts_after: @@ -1041,6 +1046,7 @@ def ligation( source_document.add_list( [composite_component_definition, composite_seq, composite_implementation] ) + final_document.add_list( [composite_component_definition, composite_seq, composite_implementation] ) From 0a2aa9110d435702c9ce317ed325f45fa4538880 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:55:06 -0600 Subject: [PATCH 12/93] Enhance README with project details and workflow Expanded the README to include detailed project information, core capabilities, and the full build workflow. --- README.md | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 98 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index cf52666..4335060 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,105 @@ It supports build functionality in comand line and cloud workflows in [SynBioSui ![PyPI - License](https://img.shields.io/pypi/l/sbol2build) ![gh-action badge](https://github.com/MyersResearchGroup/sbol2build/workflows/Python%20package/badge.svg) -## Why this project exists - -Synthetic biology design tools often stop at *design representation*. BuildCompiler closes the gap between: +# BuildCompiler -1. **Design intent** in SBOL. SBOLCanvas, Cello, and LOICA support SBOL outputs. -2. **Part/backbone selection**. Load your lab inventory to be used by BuildCompiler -3. **Assembly simulation**. Plasmids are digested to generate products by ligation. -4. **Output artifacts**. Protocols, instructions and lab automation scripts to accelerate your workflows. +BuildCompiler is an open-source framework that converts **abstract genetic designs** into **fully executable cloning workflows** using MoClo (Golden Gate) assembly. + +It bridges the gap between *design* and *build* in the Design–Build–Test–Learn (DBTL) cycle by automatically generating: + +- DNA assembly plans +- Lab automation protocols (Opentrons OT-2) +- Transformation workflows +- Plating protocols +- Step-by-step execution instructions + +👉 Instead of manually planning cloning experiments, BuildCompiler **compiles them**. + +--- + +## 🧬 What Problem Does It Solve? + +Designing genetic constructs is easy. +Building them in the lab is not. + +BuildCompiler automates the transition from: + +**SBOL design → DNA → cells on plates** + +This reduces: +- Manual planning errors +- Protocol design time +- Lab-to-lab variability + +--- + +## ⚙️ Core Capabilities + +BuildCompiler provides a complete cloning workflow composed of: + +### 1. Index Collections +Scans SBOL documents to build an internal index of available plasmids. + +### 2. Domestication +Creates missing parts as linear DNA and generates protocols to insert them into plasmids. + +### 3. Assembly Level 1 (Single Gene) +- Maps abstract parts → plasmids +- Builds a gene using MoClo +- Generates automation-ready protocols + +### 4. Assembly Level 2 (Multi-Gene) +- Combines up to 4 genes into a construct +- Uses Level 1 products as inputs + +### 5. Transformation +- Converts DNA into engineered strains +- Generates automated chemical transformation protocols + +### 6. Plating +- Creates dilution series +- Generates plating protocols + +### 7. Full Build (Orchestrator) +Runs the entire workflow automatically: +- Detects missing parts +- Generates them if needed +- Executes all assembly steps +- Chains transformation and plating + +--- + +## 🔄 Full Build Workflow + +```text +SBOL Design + ↓ +Index Collections + ↓ +Domestication + ↓ +Transformation + ↓ +Plating + ↓ +DNA Extraction + ↓ +Assembly Level 1 (Missing parts added to Domestication) + ↓ +Transformation + ↓ +Plating + ↓ +DNA Extraction + ↓ +Assembly Level 2 (Missing parts added to Assembly Level 1) + ↓ +Transformation + ↓ +Plating + ↓ +Final Build Outputs +``` ## Installing BuildCompiler: ```pip install buildcompiler``` From 56173d9c3fe6466f10e3e0407dcc6f109c7f091d Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:22:26 -0600 Subject: [PATCH 13/93] Add PRODUCT.md for BuildCompiler overview Added a comprehensive overview of the BuildCompiler tool, including its vision, target users, unique features, workflow experience, supported biology, current limitations, and roadmap. --- PRODUCT.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 PRODUCT.md diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..2d5228d --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,68 @@ +# Product Overview + +BuildCompiler is a tool for turning genetic designs into real biological builds. + +--- + +## 🎯 Vision + +Make biological construction as easy as compiling code. + +--- + +## 👥 Users + +- Synthetic biology researchers +- Biofoundries +- Automation engineers +- Computational biologists + +--- + +## 💡 What Makes It Different? + +Unlike existing tools, BuildCompiler: + +- Starts from **abstract designs** +- Automatically resolves **available parts** +- Generates **complete lab workflows** +- Supports **automation-first execution** + +--- + +## 🔁 Full Workflow Experience + +User provides: +- SBOL design +- Available plasmid collections + +BuildCompiler returns: +- Complete cloning plan +- Robot-ready protocols +- Step-by-step instructions + +--- + +## 🧬 Supported Biology + +- MoClo cloning +- Single-gene builds +- Multi-gene constructs +- Automated strain generation + +--- + +## 🚧 Current Limitations + +- Max 4 genes in Level 2 +- OT-2 only +- MoClo only + +--- + +## 🔮 Roadmap + +- Gibson / Loop assembly +- Multi-robot support +- Cloud lab integration +- LabOP standard support From 813f7d558dbeac13ea9792c2142824903b2e4eb9 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:23:18 -0600 Subject: [PATCH 14/93] Add Developer Guide for AI Agents Document the developer guide for AI agents, outlining the mental model, core entry points, contracts, constraints, testing expectations, and goals for contributions. --- AGENTS.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d0f1a31 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,61 @@ +# Developer Guide for AI Agents + +## 🧠 Mental Model + +BuildCompiler is a **workflow orchestrator**. + +Each function: +- Receives structured input +- Produces structured output +- Does not rely on hidden state + +--- + +## 🔑 Core Entry Points + +- `index_collections` +- `domestication` +- `assembly_lvl1` +- `assembly_lvl2` +- `transformation` +- `plating` +- `full_build` + +--- + +## 📦 Contracts + +Each function should: + +INPUT: +- SBOL or JSON + +OUTPUT: +- SBOL +- JSON +- Protocol files + +--- + +## ⚠️ Constraints + +- No hidden side effects +- Deterministic outputs +- Explicit file outputs + +--- + +## 🧪 Testing Expectations + +Each module must: +- Work independently +- Be testable with mock SBOL +- Validate outputs + +--- + +## 🎯 Goals for Contributions + +- Improve modularity +- Maintain pipeline clarity +- Avoid coupling biology logic with execution logic From fd65c9a8ce5c1c7087db86c8ccc8909724b01bdf Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 7 Apr 2026 16:08:31 -0600 Subject: [PATCH 15/93] type correction on ligation products --- src/buildcompiler/sbol2build.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/buildcompiler/sbol2build.py b/src/buildcompiler/sbol2build.py index 06958de..54b08ef 100644 --- a/src/buildcompiler/sbol2build.py +++ b/src/buildcompiler/sbol2build.py @@ -10,7 +10,6 @@ DNA_TYPES, ENGINEERED_INSERT, ENGINEERED_PLASMID, - ENGINEERED_REGION, FIVE_PRIME_OVERHANG, FUSION_SITES, LINEAR, @@ -1015,7 +1014,7 @@ def ligation( ) ) composite_component_definition.name = f"{composite_prefix}_{composite_number}" - composite_component_definition.addRole(ENGINEERED_REGION) + composite_component_definition.addRole(ENGINEERED_PLASMID) composite_component_definition.addType(CIRCULAR) prev_part_extract = None From ee7451f222b52a5cb2b53433c5717e4cb8677ab7 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 7 Apr 2026 16:40:51 -0600 Subject: [PATCH 16/93] function to simplify complex plasmid part representation to a scar-TU-scar-bb representation --- src/buildcompiler/buildcompiler.py | 212 ++++++++++++++++++++++++++++- 1 file changed, 209 insertions(+), 3 deletions(-) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index 01f49f4..82b4e72 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -1,7 +1,7 @@ import sbol2 import random import warnings -from typing import List, Dict +from typing import List, Dict, Tuple from buildcompiler.plasmid import Plasmid from buildcompiler.sbol2build import Assembly, dna_componentdefinition_with_sequence @@ -11,10 +11,12 @@ ) from .constants import ( AMP, + ENGINEERED_REGION, KAN, FUSION_SITES, LIGASE, PART_ROLES, + PLASMID_VECTOR, RESTRICTION_ENZYME, RESTRICTION_ENZYME_ASSEMBLY_SCAR, ENGINEERED_PLASMID, @@ -320,7 +322,7 @@ def assembly_lvl1( ) ligase_impl = self.ligase_implementations[0] - if bsaI_impl is None: + if ligase_impl is None: raise ValueError( "No appropriate ligase found in provided collections. Terminating assembly." ) @@ -338,7 +340,7 @@ def assembly_lvl1( self.indexed_plasmids.extend(composite_plasmids) - return composite_plasmids + return composite_plasmids, product_doc # TODO: Create a SBOL representation of the assembly process, updating the SBOL Document. # Using he selected parts create the representation, you need Plasmids, BsaI and T4 Ligase. @@ -606,6 +608,210 @@ def _is_single_part(self, plasmid: sbol2.ComponentDefinition) -> bool: return False + def _encapsulate_TU( + self, plasmid: Plasmid + ) -> Tuple[Plasmid, List[sbol2.ComponentDefinition]]: + """ + Collapse a detailed plasmid with a transcriptional unit (pro, rbs, cds, terminator) + into a simplified representation: + + fusion_site_L -> TU_gene -> fusion_site_R -> backbone + + Returns + ------- + Tuple[Plasmid, List[ComponentDefinition]] + simplified plasmid and any new component definitions created + """ + new_defs = [] + plasmid_def = plasmid.plasmid_definition + + fusion_left, fusion_right = plasmid.fusion_sites + left_seq = FUSION_SITES[fusion_left] + right_seq = FUSION_SITES[fusion_right] + + left_def = None + right_def = None + backbone_def = None + promoter = None + terminator = None + + # scan subcomponents for pro and term to establish range + get backbone + for comp in plasmid_def.components: + comp_def = self.sbol_doc.get(comp.definition) + + # find fusion sites of interest + if RESTRICTION_ENZYME_ASSEMBLY_SCAR in comp_def.roles: + seq_obj = self.sbol_doc.get(comp_def.sequences[0]) + if seq_obj.elements == left_seq: + left_def = comp_def + continue + if seq_obj.elements == right_seq: + right_def = comp_def + continue + elif PLASMID_VECTOR in comp_def.roles: + backbone_def = comp_def + elif sbol2.SO_PROMOTER in comp_def.roles: + promoter = comp + elif sbol2.SO_TERMINATOR in comp_def.roles: + terminator = comp + + if promoter is None or terminator is None: + raise ValueError("Could not locate promoter or terminator in plasmid TU") + + comp_dict = {c.identity: c for c in plasmid_def.components} + + follows = {} + for ( + sc + ) in plasmid_def.sequenceConstraints: # TODO replace with getInSequentialOrder? + if sc.restriction == sbol2.SBOL_RESTRICTION_PRECEDES: + subject_comp = comp_dict[sc.subject] + object_comp = comp_dict[sc.object] + follows[subject_comp] = object_comp + + old_tu_components = [] + curr_comp = promoter + + while True: + old_tu_components.append(curr_comp) + + if curr_comp.identity == terminator.identity: + break + + if curr_comp not in follows: + raise ValueError("Broken sequence constraint chain in TU") + + curr_comp = follows[curr_comp] + + tu_def = sbol2.ComponentDefinition(plasmid_def.displayId + "_TU") + tu_def.roles = [ENGINEERED_REGION] + + self.sbol_doc.add(tu_def) + new_defs.append(tu_def) + + # map old components to new ones + comp_map = {} + + for comp in old_tu_components: + new_comp = tu_def.components.create(comp.displayId) + new_comp.definition = comp.definition + comp_map[comp.identity] = new_comp.identity + + # Copy sequence annotations inside TU + for sa in plasmid_def.sequenceAnnotations: + if sa.component in comp_map: + new_sa = tu_def.sequenceAnnotations.create(sa.displayId) + new_sa.component = comp_map[sa.component] + + for loc in sa.locations: + new_range = sbol2.Range( + uri=loc.displayId, start=loc.start, end=loc.end + ) + + if hasattr(loc, "orientation"): + new_range.orientation = loc.orientation + + new_sa.locations.add(new_range) + + # Copy sequence constraints + for sc in plasmid_def.sequenceConstraints: + if sc.subject in comp_map and sc.object in comp_map: + new_sc = tu_def.sequenceConstraints.create(sc.displayId) + new_sc.subject = comp_map[sc.subject] + new_sc.object = comp_map[sc.object] + new_sc.restriction = sc.restriction + + # Build simplified plasmid definition + simple_plasmid_def = sbol2.ComponentDefinition( + plasmid_def.displayId + "_simple" + ) + self.sbol_doc.addComponentDefinition(simple_plasmid_def) + new_defs.append(simple_plasmid_def) + + simple_plasmid_def.types = list(plasmid_def.types) + simple_plasmid_def.roles = list(plasmid_def.roles) + + fusion_left_comp = simple_plasmid_def.components.create("fusion_left") + fusion_left_comp.definition = left_def.identity + + tu_comp = simple_plasmid_def.components.create("TU") + tu_comp.definition = tu_def.identity + + fusion_right_comp = simple_plasmid_def.components.create("fusion_right") + fusion_right_comp.definition = right_def.identity + + backbone_comp = None + if backbone_def: + backbone_comp = simple_plasmid_def.components.create("backbone") + backbone_comp.definition = backbone_def.identity + + # Sequence Constraints (ordering) + constraint_counter = 0 + + def add_precedes(subj, obj): + nonlocal constraint_counter + sc = simple_plasmid_def.sequenceConstraints.create( + f"constraint_{constraint_counter}" + ) + sc.subject = subj.identity + sc.object = obj.identity + sc.restriction = sbol2.SBOL_RESTRICTION_PRECEDES + constraint_counter += 1 + + add_precedes(fusion_left_comp, tu_comp) + add_precedes(tu_comp, fusion_right_comp) + + if backbone_comp: + add_precedes(fusion_right_comp, backbone_comp) + + component_map = { + left_def.identity: fusion_left_comp.identity, + tu_def.identity: tu_comp.identity, + right_def.identity: fusion_right_comp.identity, + } + + if backbone_def: + component_map[backbone_def.identity] = backbone_comp.identity + + for sa in plasmid_def.sequenceAnnotations: + comp_uri = sa.component + + if comp_uri not in component_map: + continue + + if len(sa.locations) == 0: + continue + + new_sa = simple_plasmid_def.sequenceAnnotations.create(sa.displayId) + new_sa.component = component_map[comp_uri] + + for loc in sa.locations: + if isinstance(loc, sbol2.Range): + new_loc = new_sa.locations.createRange(loc.displayId) + new_loc.start = loc.start + new_loc.end = loc.end + new_loc.orientation = loc.orientation + + elif isinstance(loc, sbol2.Cut): + new_loc = new_sa.locations.createCut(loc.displayId) + new_loc.at = loc.at + new_loc.orientation = loc.orientation + + else: + new_loc = new_sa.locations.createGenericLocation(loc.displayId) + new_loc.orientation = loc.orientation + + # Construct new plasmid object + new_plasmid = Plasmid( + simple_plasmid_def, + plasmid.strain_definitions[0], + plasmid.plasmid_implementations, + plasmid.strain_implementations, + self.sbol_doc, + ) + + return new_plasmid, new_defs + def _create_RE_implementation(name: str): pass From 5640cd8973722fbb04e58086f99245753ef34308 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Wed, 8 Apr 2026 02:12:58 -0600 Subject: [PATCH 17/93] intermediate w/ annos not working --- src/buildcompiler/buildcompiler.py | 155 +++++++++++++++++++++-------- 1 file changed, 115 insertions(+), 40 deletions(-) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index 82b4e72..71b4be0 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -610,19 +610,22 @@ def _is_single_part(self, plasmid: sbol2.ComponentDefinition) -> bool: def _encapsulate_TU( self, plasmid: Plasmid - ) -> Tuple[Plasmid, List[sbol2.ComponentDefinition]]: + ) -> Tuple[Plasmid, List[sbol2.Identified]]: """ Collapse a detailed plasmid with a transcriptional unit (pro, rbs, cds, terminator) into a simplified representation: - fusion_site_L -> TU_gene -> fusion_site_R -> backbone + fusion_site_L -> TU -> fusion_site_R -> backbone + + Builds new sequences for both the TU and simplified plasmid. Returns ------- - Tuple[Plasmid, List[ComponentDefinition]] - simplified plasmid and any new component definitions created + Tuple[Plasmid, List[Identified]] + simplified plasmid and all new SBOL objects created """ - new_defs = [] + + new_objs = [] plasmid_def = plasmid.plasmid_definition fusion_left, fusion_right = plasmid.fusion_sites @@ -639,7 +642,6 @@ def _encapsulate_TU( for comp in plasmid_def.components: comp_def = self.sbol_doc.get(comp.definition) - # find fusion sites of interest if RESTRICTION_ENZYME_ASSEMBLY_SCAR in comp_def.roles: seq_obj = self.sbol_doc.get(comp_def.sequences[0]) if seq_obj.elements == left_seq: @@ -648,10 +650,13 @@ def _encapsulate_TU( if seq_obj.elements == right_seq: right_def = comp_def continue + elif PLASMID_VECTOR in comp_def.roles: backbone_def = comp_def + elif sbol2.SO_PROMOTER in comp_def.roles: promoter = comp + elif sbol2.SO_TERMINATOR in comp_def.roles: terminator = comp @@ -661,9 +666,7 @@ def _encapsulate_TU( comp_dict = {c.identity: c for c in plasmid_def.components} follows = {} - for ( - sc - ) in plasmid_def.sequenceConstraints: # TODO replace with getInSequentialOrder? + for sc in plasmid_def.sequenceConstraints: if sc.restriction == sbol2.SBOL_RESTRICTION_PRECEDES: subject_comp = comp_dict[sc.subject] object_comp = comp_dict[sc.object] @@ -683,13 +686,38 @@ def _encapsulate_TU( curr_comp = follows[curr_comp] + def build_sequence_from_components(components): + seq = "" + ranges = {} + cursor = 1 + + for comp in components: + comp_def = self.sbol_doc.get(comp.definition) + + if not comp_def.sequences: + raise ValueError(f"{comp_def.displayId} has no sequence") + + seq_obj = self.sbol_doc.get(comp_def.sequences[0]) + part_seq = seq_obj.elements + + start = cursor + end = cursor + len(part_seq) - 1 + + ranges[comp.identity] = (start, end) + + seq += part_seq + cursor = end + 1 + + return seq, ranges + + # Create TU definition tu_def = sbol2.ComponentDefinition(plasmid_def.displayId + "_TU") tu_def.roles = [ENGINEERED_REGION] self.sbol_doc.add(tu_def) - new_defs.append(tu_def) + new_objs.append(tu_def) - # map old components to new ones + # map old components to new comp_map = {} for comp in old_tu_components: @@ -697,23 +725,43 @@ def _encapsulate_TU( new_comp.definition = comp.definition comp_map[comp.identity] = new_comp.identity - # Copy sequence annotations inside TU + # Build TU sequence + tu_seq_string, tu_ranges = build_sequence_from_components(old_tu_components) + + tu_seq = sbol2.Sequence( + tu_def.displayId + "_seq", + elements=tu_seq_string, + encoding=sbol2.SBOL_ENCODING_IUPAC, + ) + + self.sbol_doc.addSequence(tu_seq) + tu_def.sequences = [tu_seq.identity] + + new_objs.append(tu_seq) + + # Copy TU annotations for sa in plasmid_def.sequenceAnnotations: - if sa.component in comp_map: - new_sa = tu_def.sequenceAnnotations.create(sa.displayId) - new_sa.component = comp_map[sa.component] + if sa.component not in comp_map: + continue - for loc in sa.locations: - new_range = sbol2.Range( - uri=loc.displayId, start=loc.start, end=loc.end - ) + new_sa = tu_def.sequenceAnnotations.create(sa.displayId) + new_sa.component = comp_map[sa.component] + + offset_start, _ = tu_ranges[sa.component] - if hasattr(loc, "orientation"): - new_range.orientation = loc.orientation + for loc in sa.locations: + if isinstance(loc, sbol2.Range): + new_start = offset_start + loc.start - 1 + new_end = offset_start + loc.end - 1 - new_sa.locations.add(new_range) + new_loc = new_sa.locations.createRange(loc.displayId) + new_loc.start = new_start + new_loc.end = new_end + new_loc.orientation = loc.orientation - # Copy sequence constraints + # -------------------------------------------------- + # Copy TU sequence constraints + # -------------------------------------------------- for sc in plasmid_def.sequenceConstraints: if sc.subject in comp_map and sc.object in comp_map: new_sc = tu_def.sequenceConstraints.create(sc.displayId) @@ -721,12 +769,15 @@ def _encapsulate_TU( new_sc.object = comp_map[sc.object] new_sc.restriction = sc.restriction + # -------------------------------------------------- # Build simplified plasmid definition + # -------------------------------------------------- simple_plasmid_def = sbol2.ComponentDefinition( plasmid_def.displayId + "_simple" ) + self.sbol_doc.addComponentDefinition(simple_plasmid_def) - new_defs.append(simple_plasmid_def) + new_objs.append(simple_plasmid_def) simple_plasmid_def.types = list(plasmid_def.types) simple_plasmid_def.roles = list(plasmid_def.roles) @@ -745,7 +796,9 @@ def _encapsulate_TU( backbone_comp = simple_plasmid_def.components.create("backbone") backbone_comp.definition = backbone_def.identity - # Sequence Constraints (ordering) + # -------------------------------------------------- + # Sequence ordering constraints + # -------------------------------------------------- constraint_counter = 0 def add_precedes(subj, obj): @@ -764,6 +817,32 @@ def add_precedes(subj, obj): if backbone_comp: add_precedes(fusion_right_comp, backbone_comp) + # -------------------------------------------------- + # Build simplified plasmid sequence + # -------------------------------------------------- + ordered_components = [fusion_left_comp, tu_comp, fusion_right_comp] + + if backbone_comp: + ordered_components.append(backbone_comp) + + plas_seq_string, plas_ranges = build_sequence_from_components( + ordered_components + ) + + plas_seq = sbol2.Sequence( + simple_plasmid_def.displayId + "_seq", + elements=plas_seq_string, + encoding=sbol2.SBOL_ENCODING_IUPAC, + ) + + self.sbol_doc.addSequence(plas_seq) + simple_plasmid_def.sequences = [plas_seq.identity] + + new_objs.append(plas_seq) + + # -------------------------------------------------- + # Copy simplified plasmid annotations + # -------------------------------------------------- component_map = { left_def.identity: fusion_left_comp.identity, tu_def.identity: tu_comp.identity, @@ -777,31 +856,27 @@ def add_precedes(subj, obj): comp_uri = sa.component if comp_uri not in component_map: - continue - - if len(sa.locations) == 0: + print(f"{comp_uri} not found in map: {component_map}") continue new_sa = simple_plasmid_def.sequenceAnnotations.create(sa.displayId) new_sa.component = component_map[comp_uri] + offset_start, _ = plas_ranges[component_map[comp_uri]] + for loc in sa.locations: if isinstance(loc, sbol2.Range): - new_loc = new_sa.locations.createRange(loc.displayId) - new_loc.start = loc.start - new_loc.end = loc.end - new_loc.orientation = loc.orientation - - elif isinstance(loc, sbol2.Cut): - new_loc = new_sa.locations.createCut(loc.displayId) - new_loc.at = loc.at - new_loc.orientation = loc.orientation + new_start = offset_start + loc.start - 1 + new_end = offset_start + loc.end - 1 - else: - new_loc = new_sa.locations.createGenericLocation(loc.displayId) + new_loc = new_sa.locations.createRange(loc.displayId) + new_loc.start = new_start + new_loc.end = new_end new_loc.orientation = loc.orientation + # -------------------------------------------------- # Construct new plasmid object + # -------------------------------------------------- new_plasmid = Plasmid( simple_plasmid_def, plasmid.strain_definitions[0], @@ -810,7 +885,7 @@ def add_precedes(subj, obj): self.sbol_doc, ) - return new_plasmid, new_defs + return new_plasmid, new_objs def _create_RE_implementation(name: str): pass From f4a8701c6816930eaa677d4555ceb31382dd5888 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:01:16 -0600 Subject: [PATCH 18/93] Implement BuildCompiler transformation orchestration stage --- src/buildcompiler/buildcompiler.py | 201 ++++++++++++++++++++- tests/test_buildcompiler_transformation.py | 64 +++++++ 2 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 tests/test_buildcompiler_transformation.py diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index 607a242..aa0511b 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -1,7 +1,7 @@ import sbol2 import random import warnings -from typing import List, Dict +from typing import Any, Dict, List from buildcompiler.plasmid import Plasmid from buildcompiler.sbol2build import Assembly, dna_componentdefinition_with_sequence @@ -363,6 +363,145 @@ def assembly_lvl2( return protocol + def transformation( + self, + assembly_products: List[Any], + chassis_name: str = "E_coli_DH5alpha", + transformation_doc: sbol2.Document = None, + ) -> Dict[str, Any]: + """Generate deterministic transformation artifacts from assembly outputs. + + The method accepts either: + - ``Plasmid`` objects, + - ``sbol2.ComponentDefinition`` plasmids, or + - dictionaries containing at least a ``plasmid`` key with one of the above. + + :param assembly_products: Structured inputs produced by an assembly stage. + :type assembly_products: list + :param chassis_name: Display id used for the chassis module and implementation. + :type chassis_name: str + :param transformation_doc: Optional SBOL document to write outputs into. + :type transformation_doc: sbol2.Document | None + :returns: Structured transformation outputs including SBOL references, + robot JSON intermediate, protocol placeholders, and logs. + :rtype: dict + :raises ValueError: If no valid plasmid inputs can be extracted. + """ + if transformation_doc is None: + transformation_doc = self.sbol_doc + + normalized_products = self._normalize_transformation_inputs(assembly_products) + if not normalized_products: + raise ValueError("transformation requires at least one plasmid input.") + + chassis_module, chassis_impl = self._get_or_create_chassis( + transformation_doc, chassis_name + ) + + sbol_outputs = [] + robot_steps = [] + logs = [] + + for index, product in enumerate(normalized_products, start=1): + plasmid = product["plasmid"] + plasmid_impl = self._get_or_create_plasmid_implementation( + transformation_doc, plasmid + ) + transform_id = f"transform_{plasmid.displayId}_{index}" + + transformation_activity = sbol2.Activity(transform_id) + transformation_activity.name = f"Transform {chassis_name} with {plasmid.displayId}" + transformation_activity.types = "http://sbols.org/v2#build" + + chassis_usage = sbol2.Usage( + uri=f"{transform_id}_chassis_usage", + entity=chassis_impl.identity, + role="http://sbols.org/v2#build", + ) + plasmid_usage = sbol2.Usage( + uri=f"{transform_id}_plasmid_usage", + entity=plasmid_impl.identity, + role="http://sbols.org/v2#build", + ) + transformation_activity.usages = [chassis_usage, plasmid_usage] + + transformed_strain = sbol2.ModuleDefinition( + f"{chassis_name}_with_{plasmid.displayId}" + ) + transformed_strain.roles = [ORGANISM_STRAIN] + transformed_strain.name = f"{chassis_name} transformed with {plasmid.displayId}" + + chassis_module_ref = sbol2.Module( + uri=f"{transformed_strain.displayId}_chassis_module" + ) + chassis_module_ref.definition = chassis_module.identity + plasmid_fc = sbol2.FunctionalComponent( + uri=f"{transformed_strain.displayId}_plasmid_fc" + ) + plasmid_fc.definition = plasmid.identity + + transformed_strain.modules = [chassis_module_ref] + transformed_strain.functionalComponents = [plasmid_fc] + + transformed_impl = sbol2.Implementation( + f"{transformed_strain.displayId}_impl" + ) + transformed_impl.built = transformed_strain.identity + transformed_impl.wasGeneratedBy = transformation_activity.identity + + for obj in ( + transformation_activity, + chassis_usage, + plasmid_usage, + transformed_strain, + chassis_module_ref, + plasmid_fc, + transformed_impl, + ): + self._add_if_absent(transformation_doc, obj) + + sbol_outputs.append( + { + "transformation_activity": transformation_activity.identity, + "transformed_strain_module": transformed_strain.identity, + "transformed_strain_implementation": transformed_impl.identity, + } + ) + robot_steps.append( + { + "step": index, + "plasmid": plasmid.displayId, + "chassis": chassis_name, + "mix_ul": {"competent_cells": 50, "assembly_product": 5}, + "heat_shock": {"temperature_c": 42, "duration_seconds": 45}, + "recovery": {"medium": "SOC", "volume_ul": 950, "duration_min": 60}, + } + ) + logs.append( + f"Prepared transformation input for plasmid {plasmid.displayId} into chassis {chassis_name}." + ) + + return { + "stage": "transformation", + "inputs": [item["source"] for item in normalized_products], + "chassis": chassis_name, + "sbol_artifacts": sbol_outputs, + "json_intermediate": { + "protocol": "chemical_transformation", + "version": "0.1", + "steps": robot_steps, + }, + "protocol_artifacts": { + "ot2_script": "TODO: adapter to protocol generator", + "human_instructions": [ + "Thaw competent cells on ice.", + "Combine assembly product with competent cells as specified.", + "Run heat shock and recovery according to generated parameters.", + ], + "logs": logs, + }, + } + def _extract_plasmids_from_strain( self, strain: sbol2.ModuleDefinition, @@ -605,3 +744,63 @@ def _create_RE_implementation(name: str): def _create_ligase_implementation(): pass + + def _normalize_transformation_inputs( + self, assembly_products: List[Any] + ) -> List[Dict[str, Any]]: + normalized = [] + for item in assembly_products or []: + if isinstance(item, Plasmid): + normalized.append( + {"plasmid": item.plasmid_definition, "source": item.name} + ) + continue + + if isinstance(item, sbol2.ComponentDefinition): + normalized.append({"plasmid": item, "source": item.displayId}) + continue + + if isinstance(item, dict) and "plasmid" in item: + plasmid_candidate = item["plasmid"] + if isinstance(plasmid_candidate, Plasmid): + normalized.append( + { + "plasmid": plasmid_candidate.plasmid_definition, + "source": item.get("name", plasmid_candidate.name), + } + ) + elif isinstance(plasmid_candidate, sbol2.ComponentDefinition): + normalized.append( + { + "plasmid": plasmid_candidate, + "source": item.get("name", plasmid_candidate.displayId), + } + ) + return normalized + + def _get_or_create_chassis( + self, doc: sbol2.Document, chassis_name: str + ) -> tuple[sbol2.ModuleDefinition, sbol2.Implementation]: + chassis_module = doc.find(chassis_name) or sbol2.ModuleDefinition(chassis_name) + chassis_module.roles = [ORGANISM_STRAIN] + chassis_module.name = chassis_name + self._add_if_absent(doc, chassis_module) + + chassis_impl_id = f"{chassis_name}_impl" + chassis_impl = doc.find(chassis_impl_id) or sbol2.Implementation(chassis_impl_id) + chassis_impl.built = chassis_module.identity + self._add_if_absent(doc, chassis_impl) + return chassis_module, chassis_impl + + def _get_or_create_plasmid_implementation( + self, doc: sbol2.Document, plasmid: sbol2.ComponentDefinition + ) -> sbol2.Implementation: + plasmid_impl_id = f"{plasmid.displayId}_impl" + plasmid_impl = doc.find(plasmid_impl_id) or sbol2.Implementation(plasmid_impl_id) + plasmid_impl.built = plasmid.identity + self._add_if_absent(doc, plasmid_impl) + return plasmid_impl + + def _add_if_absent(self, doc: sbol2.Document, obj: Any): + if doc.find(obj.identity) is None: + doc.add(obj) diff --git a/tests/test_buildcompiler_transformation.py b/tests/test_buildcompiler_transformation.py new file mode 100644 index 0000000..0b9a6e7 --- /dev/null +++ b/tests/test_buildcompiler_transformation.py @@ -0,0 +1,64 @@ +import os +import sys +import unittest + +import sbol2 + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +from buildcompiler.buildcompiler import BuildCompiler +from buildcompiler.constants import ENGINEERED_PLASMID + + +class TestBuildCompilerTransformation(unittest.TestCase): + def setUp(self): + self.doc = sbol2.Document() + self.compiler = BuildCompiler( + collections=[], + sbh_registry="https://synbiohub.org", + auth_token="", + sbol_doc=self.doc, + ) + + def _make_plasmid(self, display_id: str) -> sbol2.ComponentDefinition: + plasmid = sbol2.ComponentDefinition(display_id) + plasmid.roles = [ENGINEERED_PLASMID] + self.doc.add(plasmid) + return plasmid + + def test_transformation_accepts_component_definitions(self): + p1 = self._make_plasmid("geneA_plasmid") + p2 = self._make_plasmid("geneB_plasmid") + + result = self.compiler.transformation([p1, p2], chassis_name="DH5alpha") + + self.assertEqual(result["stage"], "transformation") + self.assertEqual(result["chassis"], "DH5alpha") + self.assertEqual(len(result["sbol_artifacts"]), 2) + self.assertEqual(len(result["json_intermediate"]["steps"]), 2) + self.assertEqual( + result["json_intermediate"]["steps"][0]["plasmid"], "geneA_plasmid" + ) + self.assertIn("logs", result["protocol_artifacts"]) + + def test_transformation_accepts_dict_payloads(self): + plasmid = self._make_plasmid("geneC_plasmid") + result = self.compiler.transformation( + [{"name": "lvl1_geneC_output", "plasmid": plasmid}] + ) + + self.assertEqual(result["inputs"], ["lvl1_geneC_output"]) + self.assertEqual( + result["sbol_artifacts"][0]["transformed_strain_module"].endswith( + "E_coli_DH5alpha_with_geneC_plasmid/1" + ), + True, + ) + + def test_transformation_requires_inputs(self): + with self.assertRaises(ValueError): + self.compiler.transformation([]) + + +if __name__ == "__main__": + unittest.main() From d4ee967c39a551deff51827ed1987ce35122f411 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 14 Apr 2026 13:43:32 -0600 Subject: [PATCH 19/93] encapsulate TU annotaitons and sequence --- src/buildcompiler/buildcompiler.py | 44 ++++++++---------------------- 1 file changed, 11 insertions(+), 33 deletions(-) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index 71b4be0..9595fed 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -615,7 +615,7 @@ def _encapsulate_TU( Collapse a detailed plasmid with a transcriptional unit (pro, rbs, cds, terminator) into a simplified representation: - fusion_site_L -> TU -> fusion_site_R -> backbone + fusion_site_left -> TU -> fusion_site_right -> backbone Builds new sequences for both the TU and simplified plasmid. @@ -840,39 +840,17 @@ def add_precedes(subj, obj): new_objs.append(plas_seq) - # -------------------------------------------------- - # Copy simplified plasmid annotations - # -------------------------------------------------- - component_map = { - left_def.identity: fusion_left_comp.identity, - tu_def.identity: tu_comp.identity, - right_def.identity: fusion_right_comp.identity, - } - - if backbone_def: - component_map[backbone_def.identity] = backbone_comp.identity - - for sa in plasmid_def.sequenceAnnotations: - comp_uri = sa.component - - if comp_uri not in component_map: - print(f"{comp_uri} not found in map: {component_map}") - continue - - new_sa = simple_plasmid_def.sequenceAnnotations.create(sa.displayId) - new_sa.component = component_map[comp_uri] - - offset_start, _ = plas_ranges[component_map[comp_uri]] - - for loc in sa.locations: - if isinstance(loc, sbol2.Range): - new_start = offset_start + loc.start - 1 - new_end = offset_start + loc.end - 1 + for comp_uri, (start, end) in plas_ranges.items(): + anno = simple_plasmid_def.sequenceAnnotations.create( + f"simple_plasmid_def_{start}_{end}_annotation" + ) + anno.component = comp_uri - new_loc = new_sa.locations.createRange(loc.displayId) - new_loc.start = new_start - new_loc.end = new_end - new_loc.orientation = loc.orientation + location = anno.locations.createRange( + f"{simple_plasmid_def.displayId}_{start}_{end}_location" + ) + location.start = start + location.end = end # -------------------------------------------------- # Construct new plasmid object From e073084a5dff81ae18be8997e9bc5c945e3e9f32 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 14 Apr 2026 13:45:09 -0600 Subject: [PATCH 20/93] constant list of tuples defining TU fusion site order for l2 assemblies --- src/buildcompiler/constants.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/buildcompiler/constants.py b/src/buildcompiler/constants.py index 19c25a2..f39a8fe 100644 --- a/src/buildcompiler/constants.py +++ b/src/buildcompiler/constants.py @@ -23,12 +23,15 @@ "kan": KAN, "amp": AMP, } + +LVL2_FUSION_SITE_ORDER = [["A", "E"], ["E", "F"], ["F", "G"], ["G", "H"]] + # TODO http or https for identifiers? ENGINEERED_PLASMID = "http://identifiers.org/so/SO:0000637" ENGINEERED_INSERT = "https://identifiers.org/so/SO:0000915" ENGINEERED_REGION = "http://identifiers.org/so/SO:0000804" -PLASMID_VECTOR = "https://identifiers.org/so/SO:0000755" +PLASMID_VECTOR = "http://identifiers.org/so/SO:0000755" PLASMID_CLONING_VECTOR = "https://identifiers.org/ncit/NCIT:C1919" ANTIBIOTIC_RESISTANCE = "https://identifiers.org/ncit/NCIT:C17449" LIGASE = "http://identifiers.org/ncit/NCIT:C16796" From 3c26af008fe643332383f1f915388bfacc091241 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 14 Apr 2026 13:45:57 -0600 Subject: [PATCH 21/93] grab first and last fusion site now in order they appear --- src/buildcompiler/plasmid.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/buildcompiler/plasmid.py b/src/buildcompiler/plasmid.py index 1ebb600..57fada4 100644 --- a/src/buildcompiler/plasmid.py +++ b/src/buildcompiler/plasmid.py @@ -35,8 +35,7 @@ def _match_fusion_sites(self, doc: sbol2.document) -> List[str]: if seq == sequence.upper(): fusion_sites.append(key) - # fusion_sites.sort() - return fusion_sites + return [fusion_sites[0], fusion_sites[-1]] def _get_antibiotic_resistance(self, doc: sbol2.Document) -> str: for component in ( From def5b8bf92639e25c7c7fc17eaf47bf68f5e8ad9 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 14 Apr 2026 13:47:18 -0600 Subject: [PATCH 22/93] digestion now takes in Plasmid object; implementation selection happens within digestion --- src/buildcompiler/sbol2build.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/buildcompiler/sbol2build.py b/src/buildcompiler/sbol2build.py index 54b08ef..b925f6f 100644 --- a/src/buildcompiler/sbol2build.py +++ b/src/buildcompiler/sbol2build.py @@ -68,11 +68,8 @@ def run( :return: List of all composites generated, in the form of tuples of ComponentDefinition and Sequence. """ for plasmid in self.part_plasmids: - plasmid_impl = plasmid.plasmid_implementations[ - 0 - ] # TODO update with more sophisticated selection process? extracts_tuple_list, _ = part_digestion( - plasmid_impl, + plasmid, [self.restriction_enzyme], self.assembly_activity, self.source_document, @@ -322,7 +319,7 @@ def is_circular(obj: sbol2.ComponentDefinition) -> bool: def part_digestion( - reactant: sbol2.Implementation, + reactant: Plasmid, restriction_enzymes: List[sbol2.Implementation], assembly_activity: sbol2.Activity, document: sbol2.Document, @@ -339,27 +336,27 @@ def part_digestion( :param document: original SBOL2 document to be used to extract referenced objects. :return: A tuple of a list ComponentDefinitions and Sequences, and an assembly plan ModuleDefinition. """ - - reactant_component_definition = document.get(reactant.built) + reactant_impl = reactant.plasmid_implementations[0] + reactant_component_definition = reactant.plasmid_definition reactant_displayId = reactant_component_definition.displayId types = set(reactant_component_definition.types or []) if not types.intersection(DNA_TYPES): raise TypeError( - f"The reactant should have a DNA type. Types found: {reactant.types}." + f"The reactant should have a DNA type. Types found: {reactant_component_definition.types}." ) if len(reactant_component_definition.sequences) != 1: raise ValueError( - f"The reactant needs to have precisely one sequence. The input reactant has {len(reactant.sequences)} sequences" + f"The reactant needs to have precisely one sequence. The input reactant has {len(reactant_component_definition.sequences)} sequences" ) extracts_list = [] restriction_enzymes_pydna = [] assembly_activity.usages.add( sbol2.Usage( - uri=f"{reactant.displayId}", - entity=reactant.identity, + uri=f"{reactant_impl.displayId}", + entity=reactant_impl.identity, role="http://sbols.org/v2#build", ) ) From 274775850289ff4e0d4e2311d9058d505d974dfa Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 21 Apr 2026 11:31:42 -0600 Subject: [PATCH 23/93] remove todo --- src/buildcompiler/sbol2build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/buildcompiler/sbol2build.py b/src/buildcompiler/sbol2build.py index b925f6f..8f7abf3 100644 --- a/src/buildcompiler/sbol2build.py +++ b/src/buildcompiler/sbol2build.py @@ -39,7 +39,7 @@ def __init__( # TODO add fields for activity/agent/plan self, part_plasmids: List[Plasmid], backbone_plasmid: Plasmid, - restriction_enzyme: sbol2.Implementation, # TODO search for implementation in document, or domesticate the RE + restriction_enzyme: sbol2.Implementation, ligase: sbol2.Implementation, source_document: sbol2.Document, final_document: sbol2.Document, From 494ddcf1b6f73f8e540790e934ecaa7ce8cc98be Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 21 Apr 2026 16:20:12 -0600 Subject: [PATCH 24/93] ligase and RE flow optimized + domestication added if not found --- notebooks/build_compiler_test.ipynb | 529 +++++++++++++++++++++++----- src/buildcompiler/buildcompiler.py | 110 +++--- src/buildcompiler/constants.py | 2 +- 3 files changed, 513 insertions(+), 128 deletions(-) diff --git a/notebooks/build_compiler_test.ipynb b/notebooks/build_compiler_test.ipynb index d30433f..1ed655c 100644 --- a/notebooks/build_compiler_test.ipynb +++ b/notebooks/build_compiler_test.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 19, + "execution_count": 4, "id": "87bdb42e", "metadata": {}, "outputs": [], @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 5, "id": "e60a9c84", "metadata": {}, "outputs": [], @@ -25,7 +25,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 6, "id": "90648527", "metadata": {}, "outputs": [ @@ -33,65 +33,61 @@ "name": "stdout", "output_type": "stream", "text": [ - "Indexing collection: https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\n", - "Indexing collection: https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\n" + "Indexing collection: https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\n" ] } ], "source": [ - "auth = \"b9a7ee3a-5a02-42cd-ad1a-454b68cbe2a1\"\n", + "auth = \"1812840e-aa95-4588-9dc3-2a94e0bc1ed4\"\n", + "# collections = [\n", + "# \"https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\",\n", + "# \"https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\",\n", + "# ]\n", "collections = [\n", " \"https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\",\n", - " \"https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\",\n", "]\n", "buildcompiler = BuildCompiler(collections, \"https://synbiohub.org\", auth, None)" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "id": "1712f5ce", "metadata": {}, "outputs": [], "source": [ "from buildcompiler.plasmid import Plasmid\n", - "from buildcompiler.abstract_translator import get_or_pull\n", "from typing import List\n", - "from buildcompiler.constants import AMP\n", + "from buildcompiler.constants import LVL2_FUSION_SITE_ORDER, KAN, AMP\n", + "from buildcompiler.sbol2build import Assembly\n", + "import warnings\n", "\n", "\n", - "def _extract_lvl2_design_parts(\n", - " self, design_doc: sbol2.Document\n", - ") -> List[List[sbol2.ComponentDefinition]]:\n", + "def _extract_lvl2_TUs( # TODO send to misc helper file instead of buildcompiler.py?\n", + " design_doc: sbol2.Document,\n", + ") -> List[sbol2.ComponentDefinition]:\n", " \"\"\"\n", - " Returns definitions of level-0 parts grouped by level-1 components.\n", + " Returns the component definitions of each level-1 component (TU)\n", + " in the design.\n", "\n", " Args:\n", " design: :class:`sbol2.Document` containing the design.\n", "\n", " Returns:\n", - " A list where each element corresponds to a level-1 component\n", - " and contains a list of its part definitions in sequential order.\n", + " A list of TU component definitions in sequential order.\n", " \"\"\"\n", - " result = []\n", - "\n", - " design = extract_toplevel_definition(design_doc)\n", - "\n", - " for lvl1_comp in design.getInSequentialOrder():\n", - " lvl0_ordered = self.sbol_doc.get(lvl1_comp.definition).getInSequentialOrder()\n", + " top_design = extract_toplevel_definition(design_doc)\n", "\n", - " parts = [\n", - " get_or_pull(self.sbol_doc, self.sbh, lvl0_comp.definition)\n", - " for lvl0_comp in lvl0_ordered\n", - " ]\n", - "\n", - " result.append(parts)\n", - "\n", - " return result\n", + " return [\n", + " design_doc.get(comp.definition) for comp in top_design.getInSequentialOrder()\n", + " ]\n", "\n", "\n", "def assembly_lvl2(\n", - " self_buildcompiler, abstract_design_doc: sbol2.Document, backbone: Plasmid = None\n", + " self_buildcompiler,\n", + " abstract_design_doc: sbol2.Document,\n", + " backbone: Plasmid = None,\n", + " product_name: str = None,\n", ") -> list[sbol2.ComponentDefinition]:\n", " \"\"\"Assemble level-2 plasmids for the full design.\n", "\n", @@ -105,16 +101,84 @@ " # get high level genes, send to assembly_lvl1\n", " # send original abstract_design to get a new dictionary\n", " # send new dictionary to _get_backbone or get_compatible plasmids with AMP\n", - " TUs = _extract_lvl2_design_parts(self_buildcompiler, abstract_design_doc)\n", + " TUs = _extract_lvl2_TUs(abstract_design_doc)\n", + " lvl1_plasmids = []\n", + "\n", + " for i, TU in enumerate(TUs):\n", + " print(TU.displayId)\n", + "\n", + " # l1 backbone zselection\n", + " backbone_fusion_sites = LVL2_FUSION_SITE_ORDER[i]\n", + " backbone = next(\n", + " plasmid\n", + " for plasmid in self_buildcompiler.indexed_backbones\n", + " if plasmid.fusion_sites == backbone_fusion_sites\n", + " and plasmid.antibiotic_resistance == KAN\n", + " )\n", + "\n", + " print(backbone)\n", + "\n", + " # TODO insert check here to see if the TU exists already (#43). should not be too expensive, as long as we search only indexed_plasmids where AR=KAN\n", + " composite_plasmids, final_doc = self_buildcompiler.assembly_lvl1(\n", + " TU, backbone=backbone, product_name=f\"{TU.displayId}_plas\"\n", + " )\n", + "\n", + " simplified_representation, new_defs = self_buildcompiler._encapsulate_TU(\n", + " composite_plasmids[0]\n", + " )\n", + " final_doc.add_list(new_defs)\n", + " lvl1_plasmids.append(simplified_representation)\n", + " print(simplified_representation)\n", + "\n", + " final_doc.write(\"encap.xml\")\n", + "\n", + " # get l2 backbone\n", + " plasmid_dict = {}\n", + " for p in lvl1_plasmids:\n", + " key = p.plasmid_definition.displayId\n", + " plasmid_dict.setdefault(key, []).append(p)\n", + "\n", + " backbone, _ = self_buildcompiler._get_backbone(\n", + " plasmid_dict, antibiotic_resistance=AMP\n", + " )\n", + "\n", + " print(backbone)\n", "\n", - " for lvl1_comp_list in TUs:\n", - " plasmid_dict = self_buildcompiler._construct_plasmid_dict(lvl1_comp_list, AMP)\n", - " print(plasmid_dict)" + " # BbsI for l2\n", + " if self_buildcompiler.BbsI_impl is None:\n", + " self_buildcompiler._create_RE_implementation(\"BbsI\")\n", + " warnings.warn(\n", + " \"BbsI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.\",\n", + " RuntimeWarning,\n", + " )\n", + "\n", + " # TODO see about making these common enzymes (BsaI, BbSI, T4) global or class variables, so they only need to be searched for once\n", + " if self_buildcompiler.T4_ligase_impl is None:\n", + " self_buildcompiler._create_ligase_implementation()\n", + " warnings.warn(\n", + " \"No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.\",\n", + " RuntimeWarning,\n", + " )\n", + "\n", + " assembly = Assembly(\n", + " lvl1_plasmids,\n", + " backbone,\n", + " self_buildcompiler.BbsI_impl,\n", + " self_buildcompiler.T4_ligase_impl,\n", + " self_buildcompiler.sbol_doc,\n", + " final_doc,\n", + " product_name,\n", + " )\n", + "\n", + " lvl2_plasmids, final_doc = assembly.run() # TODO upload product_doc?\n", + " self_buildcompiler.indexed_plasmids.extend(lvl2_plasmids)\n", + "\n", + " return lvl2_plasmids, final_doc" ] }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 8, "id": "3e2272ff", "metadata": {}, "outputs": [ @@ -122,50 +186,366 @@ "name": "stdout", "output_type": "stream", "text": [ - "https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1\n", - "https://synbiohub.org/user/Gon/CIDARMoCloParts/B0032/1\n", - "https://synbiohub.org/user/Gon/CIDARMoCloParts/E0040m_gfp/1\n", - "https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1\n", - "https://synbiohub.org/user/Gon/CIDARMoCloParts/J23116/1\n", - "https://synbiohub.org/user/Gon/CIDARMoCloParts/B0033/1\n", - "https://synbiohub.org/user/Gon/CIDARMoCloParts/E1010m_rfp/1\n", - "https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1\n" + "Gen\n", + "Plasmid:\n", + " Name: DVK_AE_A_E\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_AE/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/DVK_AE_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'E']\n", + " Antibiotic Resistance: Kanamycin\n", + "\n", + "matched pJ23100_AB_A_B with DVK_AE_A_E on fusion site A!\n", + "matched pB0032_BC_B_C with pJ23100_AB_A_B on fusion site B!\n", + "matched pE0040_CD_C_D with pB0032_BC_B_C on fusion site C!\n", + "matched final component pB0015_DE_D_E with pE0040_CD_C_D and DVK_AE_A_E on fusion sites (D, E)!\n", + "Plasmid:\n", + " Name: Gen_plas_1_simple_A_E\n", + " Plasmid Definition: https://SBOL2Build.org/Gen_plas_1_simple/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://SBOL2Build.org/Gen_plas_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['A', 'E']\n", + " Antibiotic Resistance: Kanamycin\n", + "\n", + "Gen1\n", + "Plasmid:\n", + " Name: DVK_EF_E_F\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_EF/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/DVK_EF_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['E', 'F']\n", + " Antibiotic Resistance: Kanamycin\n", + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/ryan/GitRepo/SBOL2Build/src/buildcompiler/buildcompiler.py:310: RuntimeWarning: BsaI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.\n", + " warnings.warn(\n", + "/Users/ryan/GitRepo/SBOL2Build/src/buildcompiler/buildcompiler.py:317: RuntimeWarning: No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "matched pJ23116_EB_E_B with DVK_EF_E_F on fusion site E!\n", + "matched pB0033_BC_B_C with pJ23116_EB_E_B on fusion site B!\n", + "matched pE1010_CD_C_D with pB0033_BC_B_C on fusion site C!\n", + "matched final component pB0015_DF_D_F with pE1010_CD_C_D and DVK_EF_E_F on fusion sites (D, F)!\n", + "Plasmid:\n", + " Name: Gen1_plas_1_simple_E_F\n", + " Plasmid Definition: https://SBOL2Build.org/Gen1_plas_1_simple/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://SBOL2Build.org/Gen1_plas_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['E', 'F']\n", + " Antibiotic Resistance: Kanamycin\n", + "\n", + "matched Gen_plas_1_simple_A_E with DVA_AF_A_F on fusion site A!\n", + "matched final component Gen1_plas_1_simple_E_F with Gen_plas_1_simple_A_E and DVA_AF_A_F on fusion sites (E, F)!\n", + "Success with backbone: DVA_AF_A_F and plasmids: ['Gen_plas_1_simple_A_E', 'Gen1_plas_1_simple_E_F']\n", + "Plasmid:\n", + " Name: DVA_AF_A_F\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_AF/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/DVA_AF_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'F']\n", + " Antibiotic Resistance: Ampicillin\n", + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/ss/w4r72t4j057bp2m46gq_kjwr0000gn/T/ipykernel_72008/472231404.py:82: RuntimeWarning: BbsI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.\n", + " warnings.warn(\n" + ] + }, + { + "ename": "ValueError", + "evalue": "Not supported number of products. Found: 0", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[8]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m composite_plasmids, final_doc = \u001b[43massembly_lvl2\u001b[49m\u001b[43m(\u001b[49m\u001b[43mbuildcompiler\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdesign_doc\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[7]\u001b[39m\u001b[32m, line 106\u001b[39m, in \u001b[36massembly_lvl2\u001b[39m\u001b[34m(self_buildcompiler, abstract_design_doc, backbone, product_name)\u001b[39m\n\u001b[32m 91\u001b[39m warnings.warn(\n\u001b[32m 92\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mNo appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.\u001b[39m\u001b[33m\"\u001b[39m, \n\u001b[32m 93\u001b[39m \u001b[38;5;167;01mRuntimeWarning\u001b[39;00m,\n\u001b[32m 94\u001b[39m )\n\u001b[32m 96\u001b[39m assembly = Assembly(\n\u001b[32m 97\u001b[39m lvl1_plasmids,\n\u001b[32m 98\u001b[39m backbone,\n\u001b[32m (...)\u001b[39m\u001b[32m 103\u001b[39m product_name,\n\u001b[32m 104\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m106\u001b[39m lvl2_plasmids, final_doc = \u001b[43massembly\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# TODO upload product_doc?\u001b[39;00m\n\u001b[32m 107\u001b[39m self_buildcompiler.indexed_plasmids.extend(lvl2_plasmids)\n\u001b[32m 109\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m lvl2_plasmids, final_doc\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/GitRepo/SBOL2Build/src/buildcompiler/sbol2build.py:83\u001b[39m, in \u001b[36mAssembly.run\u001b[39m\u001b[34m(self, include_extracted_parts)\u001b[39m\n\u001b[32m 80\u001b[39m \u001b[38;5;28mself\u001b[39m.extracted_parts.append(extracts_tuple_list[\u001b[32m0\u001b[39m][\u001b[32m0\u001b[39m])\n\u001b[32m 82\u001b[39m backbone_impl = \u001b[38;5;28mself\u001b[39m.backbone.plasmid_implementations[\u001b[32m0\u001b[39m]\n\u001b[32m---> \u001b[39m\u001b[32m83\u001b[39m extracts_tuple_list, _ = \u001b[43mbackbone_digestion\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 84\u001b[39m \u001b[43m \u001b[49m\u001b[43mbackbone_impl\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 85\u001b[39m \u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mrestriction_enzyme\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 86\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43massembly_activity\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 87\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43msource_document\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 88\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 90\u001b[39m append_extracts_to_doc(extracts_tuple_list, \u001b[38;5;28mself\u001b[39m.source_document)\n\u001b[32m 91\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m include_extracted_parts:\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/GitRepo/SBOL2Build/src/buildcompiler/sbol2build.py:626\u001b[39m, in \u001b[36mbackbone_digestion\u001b[39m\u001b[34m(reactant, restriction_enzymes, assembly_activity, document)\u001b[39m\n\u001b[32m 623\u001b[39m digested_reactant = ds_reactant.cut(restriction_enzymes_pydna)\n\u001b[32m 625\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(digested_reactant) < \u001b[32m2\u001b[39m \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(digested_reactant) > \u001b[32m3\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m626\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[32m 627\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mNot supported number of products. Found: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlen\u001b[39m(digested_reactant)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 628\u001b[39m )\n\u001b[32m 629\u001b[39m \u001b[38;5;66;03m# TODO select them based on content rather than size.\u001b[39;00m\n\u001b[32m 630\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m circular \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(digested_reactant) == \u001b[32m2\u001b[39m:\n", + "\u001b[31mValueError\u001b[39m: Not supported number of products. Found: 0" ] } ], "source": [ - "assembly_lvl2(buildcompiler, design_doc)" + "composite_plasmids, final_doc = assembly_lvl2(buildcompiler, design_doc)" ] }, { "cell_type": "code", "execution_count": null, - "id": "7c12e504", + "id": "2ee945ec", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[, ]\n", - "[]\n", - "matched pJ23100_AB_A_B with DVK_AE_A_E on fusion site A!\n", - "matched pB0034_BC_B_C with pJ23100_AB_A_B on fusion site B!\n", - "matched pE0030_CD_C_D with pB0034_BC_B_C on fusion site C!\n", - "matched final component pB0015_DE_D_E with pE0030_CD_C_D and DVK_AE_A_E on fusion sites (D, E)!\n", - "Success with backbone: DVK_AE_A_E and plasmids: ['pJ23100_AB_A_B', 'pB0034_BC_B_C', 'pE0030_CD_C_D', 'pB0015_DE_D_E']\n", "[Plasmid:\n", - " Name: composite_1_A_B_C_D_E\n", - " Plasmid Definition: https://SBOL2Build.org/composite_1/1\n", + " Name: pE0040_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0040_CD/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pE0040_CD_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['C', 'D']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0034_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0034_BC/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0034_BC_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['B', 'C']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_EB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23100_EB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['E', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_AB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23100_AB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0033_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0033_BC/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0033_BC_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['B', 'C']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23106_FB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_EB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23116_EB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['E', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_GB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23116_GB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pE0030_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0030_CD/1\n", " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://SBOL2Build.org/composite_1_impl/1']\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pE0030_CD_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['C', 'D']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23100_FB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_AB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23116_AB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0032_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0032_BC/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0032_BC_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['B', 'C']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DF_D_F\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DF/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0015_DF_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'F']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DG_D_G\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DG/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0015_DG_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'G']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DH_D_H\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DH/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0015_DH_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'H']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_AB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23106_AB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_GB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23106_GB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_GB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23100_GB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pE1010_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE1010_CD/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pE1010_CD_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['C', 'D']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DE_D_E\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DE/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0015_DE_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'E']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23116_FB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_EB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23106_EB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['E', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: Gen_plas_1_A_E\n", + " Plasmid Definition: https://SBOL2Build.org/Gen_plas_1/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://SBOL2Build.org/Gen_plas_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['A', 'E']\n", + " Antibiotic Resistance: Kanamycin\n", + ", Plasmid:\n", + " Name: Gen1_plas_1_E_F\n", + " Plasmid Definition: https://SBOL2Build.org/Gen1_plas_1/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://SBOL2Build.org/Gen1_plas_1_impl/1']\n", " Strain Implementations: [None]\n", - " Fusion Sites: ['A', 'B', 'C', 'D', 'E']\n", + " Fusion Sites: ['E', 'F']\n", + " Antibiotic Resistance: Kanamycin\n", + "] [Plasmid:\n", + " Name: DVK_EF_E_F\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_EF/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/DVK_EF_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['E', 'F']\n", + " Antibiotic Resistance: Kanamycin\n", + ", Plasmid:\n", + " Name: DVK_AE_A_E\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_AE/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/DVK_AE_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'E']\n", + " Antibiotic Resistance: Kanamycin\n", + ", Plasmid:\n", + " Name: DVK_FG_F_G\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_FG/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/DVK_FG_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'G']\n", + " Antibiotic Resistance: Kanamycin\n", + ", Plasmid:\n", + " Name: DVK_GH_G_H\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_GH/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/DVK_GH_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'H']\n", " Antibiotic Resistance: Kanamycin\n", "]\n" ] } ], + "source": [ + "print(buildcompiler.indexed_plasmids, buildcompiler.indexed_backbones)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c12e504", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[, ]\n", + "[]\n" + ] + } + ], "source": [ "print(buildcompiler.restriction_enzyme_implementations)\n", "print(buildcompiler.ligase_implementations)\n", @@ -175,7 +555,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "79fd0cb5", "metadata": {}, "outputs": [], @@ -210,19 +590,7 @@ "execution_count": null, "id": "8f4ea67c", "metadata": {}, - "outputs": [ - { - "ename": "NameError", - "evalue": "name 'composite_plasmids' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mNameError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[6]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m bacterial_transformation(\u001b[43mcomposite_plasmids\u001b[49m, chassis_implementation, chassis, final_doc)\n", - "\u001b[31mNameError\u001b[39m: name 'composite_plasmids' is not defined" - ] - } - ], + "outputs": [], "source": [ "# bacterial_transformation(composite_plasmids, chassis_implementation, chassis, final_doc)" ] @@ -230,23 +598,10 @@ { "cell_type": "code", "execution_count": null, - "id": "5553bfc3", + "id": "cd334add", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'Valid.'" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# final_doc.write(\"fullbuild.xml\")" - ] + "outputs": [], + "source": [] } ], "metadata": { diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index 9595fed..a40a1dc 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -4,7 +4,11 @@ from typing import List, Dict, Tuple from buildcompiler.plasmid import Plasmid -from buildcompiler.sbol2build import Assembly, dna_componentdefinition_with_sequence +from buildcompiler.sbol2build import ( + Assembly, + dna_componentdefinition_with_sequence, + rebase_restriction_enzyme, +) from .abstract_translator import ( get_or_pull, get_compatible_plasmids, @@ -50,8 +54,9 @@ def __init__( self.sbol_doc = sbol_doc or sbol2.Document() self.indexed_plasmids = [] self.indexed_backbones = [] - self.restriction_enzyme_implementations = [] - self.ligase_implementations = [] + self.BsaI_impl = None + self.BbsI_impl = None + self.T4_ligase_impl = None self._index_collections(collections) @@ -110,9 +115,18 @@ def _index_collections(self, collections: List[str]): ) elif sbol2.BIOPAX_PROTEIN in built_object.types: if RESTRICTION_ENZYME in built_object.roles: - self.restriction_enzyme_implementations.append(implementation) + if ( + built_object.definition + == "http://rebase.neb.com/rebase/enz/BsaI.html" + ): + self.BsaI_impl = implementation + elif ( + built_object.definition + == "http://rebase.neb.com/rebase/enz/BbsI.html" + ): + self.BbsI_impl = implementation elif LIGASE in built_object.roles: - self.ligase_implementations.append(implementation) + self.T4_ligase_impl = implementation for strain in self.sbol_doc.moduleDefinitions: if ORGANISM_STRAIN in strain.roles: @@ -159,28 +173,18 @@ def _remove_internal_bsai_sites(sequence: str) -> tuple[str, int]: removals += 1 return domesticated_sequence, removals - bsaI_impl = next( - ( - impl - for impl in self.restriction_enzyme_implementations - if self.sbol_doc.find(impl.built).displayId == "BsaI" - ), - None, - ) - if bsaI_impl is None: + if self.BsaI_impl is None: self._create_RE_implementation("BsaI") warnings.warn( "BsaI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.", RuntimeWarning, ) - ligase_impl = ( - self.ligase_implementations[0] if self.ligase_implementations else None - ) - if ligase_impl is None: + if self.T4_ligase_impl is None: self._create_ligase_implementation() warnings.warn( - "No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol." + "No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.", + RuntimeWarning, ) dsDNAs = [] @@ -269,8 +273,8 @@ def _remove_internal_bsai_sites(sequence: str) -> tuple[str, int]: assembly = Assembly( [Plasmid(insert_definition, None, [insert_impl], [], self.sbol_doc)], backbone, - bsaI_impl, - ligase_impl, + self.bsaI_impl, + self.T4_ligase_impl, self.sbol_doc, ) assembly_products, assembly_doc = assembly.run() @@ -311,27 +315,25 @@ def assembly_lvl1( else: compatible_plasmids = get_compatible_plasmids(plasmid_dict, backbone) - bsaI_impl = next( - impl - for impl in self.restriction_enzyme_implementations - if self.sbol_doc.find(impl.built).displayId == "BsaI" - ) - if bsaI_impl is None: - raise ValueError( - "BsaI Restriction enzyme not found in provided collections. Terminating assembly." + if self.BsaI_impl is None: + self._create_RE_implementation("BsaI") + warnings.warn( + "BsaI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.", + RuntimeWarning, ) - ligase_impl = self.ligase_implementations[0] - if ligase_impl is None: - raise ValueError( - "No appropriate ligase found in provided collections. Terminating assembly." + if self.T4_ligase_impl is None: + self._create_ligase_implementation() + warnings.warn( + "No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.", + RuntimeWarning, ) assembly = Assembly( compatible_plasmids, backbone, - bsaI_impl, - ligase_impl, + self.BsaI_impl, + self.T4_ligase_impl, self.sbol_doc, final_doc, product_name, @@ -483,7 +485,6 @@ def _get_backbone( for backbone in sorted_backbones: if backbone.antibiotic_resistance == antibiotic_resistance: # check for compatibility - # also, if we find a hit here we may not need to run get_compatible plasmids later, work is already done try: compatible_plasmids = get_compatible_plasmids( plasmid_dict, backbone @@ -865,8 +866,37 @@ def add_precedes(subj, obj): return new_plasmid, new_objs - def _create_RE_implementation(name: str): - pass + def _create_RE_implementation(self, name: str): + RE_def = rebase_restriction_enzyme(name) + + RE_sourcing = sbol2.Activity(f"{name}_restriction_enzyme_purchase") + RE_sourcing.name = "Restriction Enzyme Purchase" + + RE_impl = sbol2.Implementation(f"{RE_def.displayId}_impl") + + RE_impl.built = RE_def.identity + RE_impl.wasGeneratedBy = RE_sourcing.identity + + self.sbol_doc.add_list([RE_impl, RE_def]) + + if name == "BsaI": + self.BsaI_impl = RE_impl + elif name == "BbsI": + self.BbsI_impl = RE_impl + + def _create_ligase_implementation(self): + ligase_def = sbol2.ComponentDefinition("T4_Ligase") + ligase_def.name = "T4_Ligase" + ligase_def.types = [sbol2.BIOPAX_PROTEIN] + ligase_def.roles = ["http://identifiers.org/ncit/NCIT:C16796"] + + ligase_sourcing = sbol2.Activity("ligase_purchase") + ligase_sourcing.name = "Ligase Purchase" + + T4_impl = sbol2.Implementation(f"{ligase_def.displayId}_impl") + + T4_impl.built = ligase_def.identity + T4_impl.wasGeneratedBy = ligase_sourcing.identity - def _create_ligase_implementation(): - pass + self.sbol_doc.add_list([T4_impl, ligase_def]) + self.T4_ligase_impl = T4_impl diff --git a/src/buildcompiler/constants.py b/src/buildcompiler/constants.py index f39a8fe..e455c66 100644 --- a/src/buildcompiler/constants.py +++ b/src/buildcompiler/constants.py @@ -26,7 +26,7 @@ LVL2_FUSION_SITE_ORDER = [["A", "E"], ["E", "F"], ["F", "G"], ["G", "H"]] -# TODO http or https for identifiers? +# TODO CHANGE ALL TO HTTP ENGINEERED_PLASMID = "http://identifiers.org/so/SO:0000637" ENGINEERED_INSERT = "https://identifiers.org/so/SO:0000915" From 69fea09856a2cf591d72dec16a75876ca63aa97a Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 21 Apr 2026 16:33:36 -0600 Subject: [PATCH 25/93] definition->identity --- notebooks/build_compiler_test.ipynb | 28 ++++++++++++---------------- src/buildcompiler/buildcompiler.py | 4 ++-- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/notebooks/build_compiler_test.ipynb b/notebooks/build_compiler_test.ipynb index 1ed655c..caea5e6 100644 --- a/notebooks/build_compiler_test.ipynb +++ b/notebooks/build_compiler_test.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 4, + "execution_count": 1, "id": "87bdb42e", "metadata": {}, "outputs": [], @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 2, "id": "e60a9c84", "metadata": {}, "outputs": [], @@ -25,7 +25,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 3, "id": "90648527", "metadata": {}, "outputs": [ @@ -33,25 +33,23 @@ "name": "stdout", "output_type": "stream", "text": [ - "Indexing collection: https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\n" + "Indexing collection: https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\n", + "Indexing collection: https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\n" ] } ], "source": [ "auth = \"1812840e-aa95-4588-9dc3-2a94e0bc1ed4\"\n", - "# collections = [\n", - "# \"https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\",\n", - "# \"https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\",\n", - "# ]\n", "collections = [\n", " \"https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\",\n", + " \"https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\",\n", "]\n", "buildcompiler = BuildCompiler(collections, \"https://synbiohub.org\", auth, None)" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 4, "id": "1712f5ce", "metadata": {}, "outputs": [], @@ -178,7 +176,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 5, "id": "3e2272ff", "metadata": {}, "outputs": [ @@ -225,9 +223,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/Users/ryan/GitRepo/SBOL2Build/src/buildcompiler/buildcompiler.py:310: RuntimeWarning: BsaI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.\n", - " warnings.warn(\n", - "/Users/ryan/GitRepo/SBOL2Build/src/buildcompiler/buildcompiler.py:317: RuntimeWarning: No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.\n", + "/Users/ryan/GitRepo/SBOL2Build/src/buildcompiler/buildcompiler.py:320: RuntimeWarning: BsaI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.\n", " warnings.warn(\n" ] }, @@ -266,7 +262,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "/var/folders/ss/w4r72t4j057bp2m46gq_kjwr0000gn/T/ipykernel_72008/472231404.py:82: RuntimeWarning: BbsI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.\n", + "/var/folders/ss/w4r72t4j057bp2m46gq_kjwr0000gn/T/ipykernel_78396/3312876232.py:92: RuntimeWarning: BbsI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.\n", " warnings.warn(\n" ] }, @@ -277,8 +273,8 @@ "traceback": [ "\u001b[31m---------------------------------------------------------------------------\u001b[39m", "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[8]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m composite_plasmids, final_doc = \u001b[43massembly_lvl2\u001b[49m\u001b[43m(\u001b[49m\u001b[43mbuildcompiler\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdesign_doc\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[7]\u001b[39m\u001b[32m, line 106\u001b[39m, in \u001b[36massembly_lvl2\u001b[39m\u001b[34m(self_buildcompiler, abstract_design_doc, backbone, product_name)\u001b[39m\n\u001b[32m 91\u001b[39m warnings.warn(\n\u001b[32m 92\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mNo appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.\u001b[39m\u001b[33m\"\u001b[39m, \n\u001b[32m 93\u001b[39m \u001b[38;5;167;01mRuntimeWarning\u001b[39;00m,\n\u001b[32m 94\u001b[39m )\n\u001b[32m 96\u001b[39m assembly = Assembly(\n\u001b[32m 97\u001b[39m lvl1_plasmids,\n\u001b[32m 98\u001b[39m backbone,\n\u001b[32m (...)\u001b[39m\u001b[32m 103\u001b[39m product_name,\n\u001b[32m 104\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m106\u001b[39m lvl2_plasmids, final_doc = \u001b[43massembly\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# TODO upload product_doc?\u001b[39;00m\n\u001b[32m 107\u001b[39m self_buildcompiler.indexed_plasmids.extend(lvl2_plasmids)\n\u001b[32m 109\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m lvl2_plasmids, final_doc\n", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[5]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m composite_plasmids, final_doc = \u001b[43massembly_lvl2\u001b[49m\u001b[43m(\u001b[49m\u001b[43mbuildcompiler\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdesign_doc\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[4]\u001b[39m\u001b[32m, line 115\u001b[39m, in \u001b[36massembly_lvl2\u001b[39m\u001b[34m(self_buildcompiler, abstract_design_doc, backbone, product_name)\u001b[39m\n\u001b[32m 100\u001b[39m warnings.warn(\n\u001b[32m 101\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mNo appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 102\u001b[39m \u001b[38;5;167;01mRuntimeWarning\u001b[39;00m,\n\u001b[32m 103\u001b[39m )\n\u001b[32m 105\u001b[39m assembly = Assembly(\n\u001b[32m 106\u001b[39m lvl1_plasmids,\n\u001b[32m 107\u001b[39m backbone,\n\u001b[32m (...)\u001b[39m\u001b[32m 112\u001b[39m product_name,\n\u001b[32m 113\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m115\u001b[39m lvl2_plasmids, final_doc = \u001b[43massembly\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# TODO upload product_doc?\u001b[39;00m\n\u001b[32m 116\u001b[39m self_buildcompiler.indexed_plasmids.extend(lvl2_plasmids)\n\u001b[32m 118\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m lvl2_plasmids, final_doc\n", "\u001b[36mFile \u001b[39m\u001b[32m~/GitRepo/SBOL2Build/src/buildcompiler/sbol2build.py:83\u001b[39m, in \u001b[36mAssembly.run\u001b[39m\u001b[34m(self, include_extracted_parts)\u001b[39m\n\u001b[32m 80\u001b[39m \u001b[38;5;28mself\u001b[39m.extracted_parts.append(extracts_tuple_list[\u001b[32m0\u001b[39m][\u001b[32m0\u001b[39m])\n\u001b[32m 82\u001b[39m backbone_impl = \u001b[38;5;28mself\u001b[39m.backbone.plasmid_implementations[\u001b[32m0\u001b[39m]\n\u001b[32m---> \u001b[39m\u001b[32m83\u001b[39m extracts_tuple_list, _ = \u001b[43mbackbone_digestion\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 84\u001b[39m \u001b[43m \u001b[49m\u001b[43mbackbone_impl\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 85\u001b[39m \u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mrestriction_enzyme\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 86\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43massembly_activity\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 87\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43msource_document\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 88\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 90\u001b[39m append_extracts_to_doc(extracts_tuple_list, \u001b[38;5;28mself\u001b[39m.source_document)\n\u001b[32m 91\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m include_extracted_parts:\n", "\u001b[36mFile \u001b[39m\u001b[32m~/GitRepo/SBOL2Build/src/buildcompiler/sbol2build.py:626\u001b[39m, in \u001b[36mbackbone_digestion\u001b[39m\u001b[34m(reactant, restriction_enzymes, assembly_activity, document)\u001b[39m\n\u001b[32m 623\u001b[39m digested_reactant = ds_reactant.cut(restriction_enzymes_pydna)\n\u001b[32m 625\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(digested_reactant) < \u001b[32m2\u001b[39m \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(digested_reactant) > \u001b[32m3\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m626\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[32m 627\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mNot supported number of products. Found: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlen\u001b[39m(digested_reactant)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 628\u001b[39m )\n\u001b[32m 629\u001b[39m \u001b[38;5;66;03m# TODO select them based on content rather than size.\u001b[39;00m\n\u001b[32m 630\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m circular \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(digested_reactant) == \u001b[32m2\u001b[39m:\n", "\u001b[31mValueError\u001b[39m: Not supported number of products. Found: 0" diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index a40a1dc..4119eb2 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -116,12 +116,12 @@ def _index_collections(self, collections: List[str]): elif sbol2.BIOPAX_PROTEIN in built_object.types: if RESTRICTION_ENZYME in built_object.roles: if ( - built_object.definition + built_object.identity == "http://rebase.neb.com/rebase/enz/BsaI.html" ): self.BsaI_impl = implementation elif ( - built_object.definition + built_object.identity == "http://rebase.neb.com/rebase/enz/BbsI.html" ): self.BbsI_impl = implementation From d9fbdf20605587038e60d9b49947178ae801b093 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Mon, 27 Apr 2026 14:46:04 -0600 Subject: [PATCH 26/93] lvl2 and multi-design lvl1 drafts --- src/buildcompiler/buildcompiler.py | 179 ++++++++++++++++++++++------- 1 file changed, 135 insertions(+), 44 deletions(-) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index 4119eb2..7b2323e 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -12,6 +12,7 @@ from .abstract_translator import ( get_or_pull, get_compatible_plasmids, + extract_toplevel_definition, ) from .constants import ( AMP, @@ -19,6 +20,7 @@ KAN, FUSION_SITES, LIGASE, + LVL2_FUSION_SITE_ORDER, PART_ROLES, PLASMID_VECTOR, RESTRICTION_ENZYME, @@ -285,11 +287,11 @@ def _remove_internal_bsai_sites(sequence: str) -> tuple[str, int]: def assembly_lvl1( self, - abstract_design: sbol2.ComponentDefinition, + abstract_designs: List[sbol2.ComponentDefinition], final_doc: sbol2.Document = sbol2.Document(), product_name: str = None, backbone: Plasmid = None, - ) -> list[sbol2.ComponentDefinition]: + ) -> Tuple[Dict, sbol2.Document]: """Assemble level-1 plasmids for each gene/transcriptional unit. Uses indexed plasmids/backbones and the current design to assemble @@ -300,49 +302,51 @@ def assembly_lvl1( :raises LookupError: If compatible plasmids or backbones cannot be found. """ - # TODO: Identify parts from the abstract design needed for lvl1 assembly and find compatible indexed plasmids/backbones. - # if backbone provided then use it.Then look for parts constraind by the backbone fusion sites. - # else, run an algorithm to try a backbone from 4 the choices. If it fails on the 4 raise an error. - - plasmid_dict = self._get_input_plasmids( - design=abstract_design, antibiotic_resistance=AMP - ) + assembly_dict = {} - if not backbone: - backbone, compatible_plasmids = self._get_backbone( - plasmid_dict, antibiotic_resistance=KAN + for abstract_design in abstract_designs: + plasmid_dict = self._get_input_plasmids( + design=abstract_design, antibiotic_resistance=AMP ) - else: - compatible_plasmids = get_compatible_plasmids(plasmid_dict, backbone) - if self.BsaI_impl is None: - self._create_RE_implementation("BsaI") - warnings.warn( - "BsaI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.", - RuntimeWarning, - ) + if not backbone: + backbone, compatible_plasmids = self._get_backbone( + plasmid_dict, antibiotic_resistance=KAN + ) + else: + compatible_plasmids = get_compatible_plasmids(plasmid_dict, backbone) + + if self.BsaI_impl is None: + self._create_RE_implementation("BsaI") + warnings.warn( + "BsaI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.", + RuntimeWarning, + ) - if self.T4_ligase_impl is None: - self._create_ligase_implementation() - warnings.warn( - "No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.", - RuntimeWarning, - ) + if self.T4_ligase_impl is None: + self._create_ligase_implementation() + warnings.warn( + "No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.", + RuntimeWarning, + ) - assembly = Assembly( - compatible_plasmids, - backbone, - self.BsaI_impl, - self.T4_ligase_impl, - self.sbol_doc, - final_doc, - product_name, - ) - composite_plasmids, product_doc = assembly.run() # TODO upload product_doc? + assembly = Assembly( + compatible_plasmids, + backbone, + self.BsaI_impl, + self.T4_ligase_impl, + self.sbol_doc, + final_doc, + product_name, + ) + composite_plasmids, product_doc = assembly.run() # TODO upload product_doc? - self.indexed_plasmids.extend(composite_plasmids) + self.indexed_plasmids.extend( + composite_plasmids + ) # see about using a wrapper function to do this, where it checks if the design already exists (like in index_collections). this way we avoid duplicate issues that might come with loading the abstract design definitions into the self.sbol_doc ahead of time + assembly_dict[abstract_design.identity] = composite_plasmids - return composite_plasmids, product_doc + return assembly_dict, product_doc # TODO: Create a SBOL representation of the assembly process, updating the SBOL Document. # Using he selected parts create the representation, you need Plasmids, BsaI and T4 Ligase. @@ -354,6 +358,9 @@ def assembly_lvl1( def assembly_lvl2( self, + abstract_design_doc: sbol2.Document, + backbone: Plasmid = None, + product_name: str = None, ) -> list[sbol2.ComponentDefinition]: """Assemble level-2 plasmids for the full design. @@ -364,14 +371,78 @@ def assembly_lvl2( :rtype: list[Plasmid] :raises LookupError: If compatible plasmids or backbones cannot be found. """ + # get high level genes, send to assembly_lvl1 + # send original abstract_design to get a new dictionary + # send new dictionary to _get_backbone or get_compatible plasmids with AMP + TUs = _extract_lvl2_TUs(abstract_design_doc) + lvl1_plasmids = [] - # TODO: Identify parts from the abstract design needed for lvl2 assembly and find compatible indexed plasmids/backbones. - # TODO: Create a SBOL representation of the assembly process, updating the SBOL Document. - # TODO: Generate a protocol for the assembly process. - protocol = "To be implemented by PUDU" - # TODO: Updates indexed plasmids with assembled versions. + for i, TU in enumerate(TUs): + print(TU.displayId) - return protocol + # l1 backbone zselection + backbone_fusion_sites = LVL2_FUSION_SITE_ORDER[i] + backbone = next( + plasmid + for plasmid in self.indexed_backbones + if plasmid.fusion_sites == backbone_fusion_sites + and plasmid.antibiotic_resistance == KAN + ) + + print(backbone) + + # TODO insert check here to see if the TU exists already (#43). should not be too expensive, as long as we search only indexed_plasmids where AR=KAN + composite_plasmids, final_doc = self.assembly_lvl1( + TU, backbone=backbone, product_name=f"{TU.displayId}_plas" + ) + + simplified_representation, new_defs = self._encapsulate_TU( + composite_plasmids[0] + ) + final_doc.add_list(new_defs) + lvl1_plasmids.append(simplified_representation) + print(simplified_representation) + + # get l2 backbone + plasmid_dict = {} + for p in lvl1_plasmids: + key = p.plasmid_definition.displayId + plasmid_dict.setdefault(key, []).append(p) + + backbone, _ = self._get_backbone(plasmid_dict, antibiotic_resistance=AMP) + + print(backbone) + + # BbsI for l2 + if self.BbsI_impl is None: + self._create_RE_implementation("BbsI") + warnings.warn( + "BbsI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.", + RuntimeWarning, + ) + + # TODO see about making these common enzymes (BsaI, BbSI, T4) global or class variables, so they only need to be searched for once + if self.T4_ligase_impl is None: + self._create_ligase_implementation() + warnings.warn( + "No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.", + RuntimeWarning, + ) + + assembly = Assembly( + lvl1_plasmids, + backbone, + self.BbsI_impl, + self.T4_ligase_impl, + self.sbol_doc, + final_doc, + product_name, + ) + + lvl2_plasmids, final_doc = assembly.run() # TODO upload product_doc? + self.indexed_plasmids.extend(lvl2_plasmids) + + return lvl2_plasmids, final_doc def _extract_plasmids_from_strain( self, @@ -900,3 +971,23 @@ def _create_ligase_implementation(self): self.sbol_doc.add_list([T4_impl, ligase_def]) self.T4_ligase_impl = T4_impl + + +def _extract_lvl2_TUs( # TODO send to misc helper file instead of buildcompiler.py? + design_doc: sbol2.Document, +) -> List[sbol2.ComponentDefinition]: + """ + Returns the component definitions of each level-1 component (TU) + in the design. + + Args: + design: :class:`sbol2.Document` containing the design. + + Returns: + A list of TU component definitions in sequential order. + """ + top_design = extract_toplevel_definition(design_doc) + + return [ + design_doc.get(comp.definition) for comp in top_design.getInSequentialOrder() + ] From 88a3b44b5c759abce9ef91fc4e3aea800bc632f0 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Mon, 27 Apr 2026 15:17:42 -0600 Subject: [PATCH 27/93] docstring updates --- src/buildcompiler/sbol2build.py | 122 ++++++++++++++++++++++---------- 1 file changed, 83 insertions(+), 39 deletions(-) diff --git a/src/buildcompiler/sbol2build.py b/src/buildcompiler/sbol2build.py index 8f7abf3..1de23c9 100644 --- a/src/buildcompiler/sbol2build.py +++ b/src/buildcompiler/sbol2build.py @@ -28,11 +28,18 @@ class Assembly: """Creates an Assembly Plan. - :param name: Name of the assembly plan ModuleDefinition. - :param part_plasmids: Parts in backbone to be assembled. - :param plasmid_acceptor_backbone: Backbone in which parts are inserted on the assembly. - :param restriction_enzyme: Restriction enzyme name used by PyDNA. Case sensitive, follow standard restriction enzyme nomenclature, i.e. 'BsaI' - :param document: SBOL Document where the assembly plan will be created. + :param part_plasmids: List of part-in-backbone plasmids to be assembled. + :param backbone_plasmid: Acceptor backbone into which parts are inserted. + :param restriction_enzyme: SBOL Implementation representing the restriction enzyme + (e.g. BsaI) used to digest parts during assembly. + :param ligase: SBOL Implementation representing the ligase (e.g. T4) used to + ligate digested parts. + :param source_document: SBOL Document containing the source part/plasmid definitions. + :param final_document: SBOL Document where assembled composite plasmid definitions + will be written. + :param composite_prefix: Prefix used when naming composite plasmid definitions. + Defaults to 'composite'. + """ def __init__( # TODO add fields for activity/agent/plan @@ -58,14 +65,27 @@ def __init__( # TODO add fields for activity/agent/plan def run( self, include_extracted_parts: bool = False - ) -> List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]]: - """Runs full assembly simulation. - - `document` parameter of golden_gate_assembly_plan object is updated by reference to include assembly plan ModuleDefinition and all related information. - - Runs :func:`part_digestion` for all `part_plasmids` and :func:`backbone_digestion` for `plasmid_acceptor_backbone` with `restriction_enzyme`. Then runs :func:`ligation` with these parts to form composites. - - :return: List of all composites generated, in the form of tuples of ComponentDefinition and Sequence. + ) -> Tuple[List[Plasmid], sbol2.Document]: + """Run the full Golden Gate assembly simulation. + + Executes the following steps in order: + + 1. Calls :func:`part_digestion` on each plasmid in ``part_plasmids`` using + ``restriction_enzyme``, appending extracted parts to ``source_document``. + 2. Calls :func:`backbone_digestion` on the first implementation of ``backbone``, + appending the linearised backbone to ``source_document``. + 3. Calls :func:`ligation` on all extracted parts and the backbone to produce + composite plasmid implementations, written to ``final_document``. + 4. Wraps each composite implementation in a :class:`Plasmid` object and returns + the full list alongside the populated ``final_document``. + + :param include_extracted_parts: If ``True``, extracted part and backbone + definitions are also written to ``final_document`` in addition to + ``source_document``. Defaults to ``False``. + :return: A tuple of (composite plasmids, final document), where composite + plasmids is a list of :class:`Plasmid` objects built from the ligated + implementations, and final document is the populated ``sbol2.Document`` + containing all assembly outputs. """ for plasmid in self.part_plasmids: extracts_tuple_list, _ = part_digestion( @@ -324,17 +344,28 @@ def part_digestion( assembly_activity: sbol2.Activity, document: sbol2.Document, ) -> Tuple[List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]]]: - """Runs a simulated digestion on the top level sequence in the reactant ComponentDefinition or ModuleDefinition with the given restriciton enzymes, creating a extracted part ComponentDefinition, a digestion Interaction, and converts existing scars to 5' and 3' overhangs. - The product ComponentDefinition is assumed the digested part in this case. - - Written for use with the SBOL2.3 output of https://sbolcanvas.org - - :param reactant: Plasmid DNA to be digested as SBOL ComponentDefinition - :param restriction_enzymes: Restriction enzymes as :class:`sbol2.ComponentDefinition` - (generate with :func:`rebase_restriction_enzyme`). - :param assembly_plan: SBOL ModuleDefinition to contain the functional components, interactions, and participations - :param document: original SBOL2 document to be used to extract referenced objects. - :return: A tuple of a list ComponentDefinitions and Sequences, and an assembly plan ModuleDefinition. + """Simulate restriction digestion of a part plasmid and extract the insert. + + Uses PyDNA to cut the reactant sequence, then constructs SBOL representations + of the extracted part, its 5' and 3' overhangs, and any derived scar sequences. + Each enzyme and the reactant implementation are recorded as usages on + ``assembly_activity``. + + Expects the reactant to be circular with 2 digest products, or linear with 3 + (backbone | part | backbone). The shorter circular product or middle linear + product is taken as the extracted insert. + + :param reactant: Part-in-backbone plasmid to digest. + :param restriction_enzymes: Restriction enzyme implementations; the corresponding + ``ComponentDefinition.name`` must match a PyDNA/ReBase enzyme name (e.g. ``'BsaI'``). + :param assembly_activity: SBOL Activity to record reactant and enzyme usages on. + :param document: Source SBOL document used to resolve referenced definitions and sequences. + :return: A tuple of (extracts, activity), where extracts is a list of + ``(ComponentDefinition, Sequence)`` pairs covering the extracted part, + overhangs, and scar definitions, and activity is the updated ``assembly_activity``. + :raises TypeError: If the reactant has no recognised DNA type. + :raises ValueError: If the reactant does not have exactly one sequence, or if + the number of digest products is unsupported for the reactant topology. """ reactant_impl = reactant.plasmid_implementations[0] reactant_component_definition = reactant.plasmid_definition @@ -548,17 +579,27 @@ def backbone_digestion( assembly_activity: sbol2.Activity, document: sbol2.Document, ) -> Tuple[List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]]]: - """Runs a simulated digestion on the top level sequence in the reactant ComponentDefinition or ModuleDefinition with the given restriciton enzymes, creating an open backbone ComponentDefinition, a digestion Interaction, and converts existing scars to 5' and 3' overhangs. - The product ComponentDefinition is assumed the open backbone in this case. - - Written for use with the SBOL2.3 output of https://sbolcanvas.org - - :param reactant: DNA to be digested as SBOL ComponentDefinition or ModuleDefinition, usually a part_in_backbone. ComponentDefinition is the best-practice type for plasmids. - :param restriction_enzymes: Restriction enzymes as :class:`sbol2.ComponentDefinition` - (generate with :func:`rebase_restriction_enzyme`). - :param assembly_plan: SBOL ModuleDefinition to contain the functional components, interactions, and participations - :param document: original SBOL2 document to be used to extract referenced objects. - :return: A tuple of a list ComponentDefinitions and Sequences, and an assembly plan ModuleDefinition. + """Simulate restriction digestion of a backbone plasmid and extract the linearised vector. + + Mirrors :func:`part_digestion` but targets the backbone: for a circular reactant + with 2 digest products the longer fragment is taken as the open backbone; for a + linear reactant with 3 products the outer prefix/suffix fragments are used. + The resulting open-backbone ``ComponentDefinition``, its 5' and 3' overhangs, and + any matched scar sequences are returned as SBOL objects. The reactant implementation + and each enzyme are recorded as usages on ``assembly_activity``. + + :param reactant: SBOL Implementation whose ``built`` URI resolves to the + backbone ``ComponentDefinition`` in ``document``. + :param restriction_enzymes: Restriction enzyme implementations; the corresponding + ``ComponentDefinition.name`` must match a PyDNA/ReBase enzyme name (e.g. ``'BsaI'``). + :param assembly_activity: SBOL Activity to record reactant and enzyme usages on. + :param document: Source SBOL document used to resolve referenced definitions and sequences. + :return: A tuple of (extracts, activity), where extracts is a list of + ``(ComponentDefinition, Sequence)`` pairs covering the open backbone, + overhangs, and scar definitions, and activity is the updated ``assembly_activity``. + :raises TypeError: If the reactant has no recognised DNA type. + :raises ValueError: If the reactant does not have exactly one sequence, or if + the number of digest products is unsupported for the reactant topology. """ reactant_component_definition = document.get(reactant.built) reactant_displayId = reactant_component_definition.displayId @@ -775,11 +816,14 @@ def ligation( ) -> List[sbol2.Implementation]: """Ligates Components using base complementarity and creates product Components and a ligation Interaction. - :param reactants: DNA parts to be ligated as SBOL ModuleDefinition. + :param reactants: Extracted part and backbone ``ComponentDefinition`` objects to ligate. :param assembly_activity: SBOL activity to track assembly inputs & outputs - :param document: SBOL2 document containing all reactant ComponentDefinitions. - :param ligase: as SBOL Implementation - :return: List of all composites generated, in the form of tuples of ComponentDefinition and Sequence. + :param composite_prefix: Prefix used when naming composite ``ComponentDefinition`` + and ``Implementation`` identities. + :param source_document: SBOL Document containing all reactant definitions. + :param final_document: SBOL Document that receives composite definitions and implementations. + :param ligase: SBOL Implementation of the ligase (e.g. T4). + :return: List of ``sbol2.Implementation`` objects, one per composite plasmid generated. """ enzyme_definition = source_document.get(ligase.built) From 6bce1f03ef967018877723dd8f3627ce82559fe3 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:55:13 -0600 Subject: [PATCH 28/93] Add plating workflow with SBOL/protocol artifact generation --- pyproject.toml | 5 + src/buildcompiler/buildcompiler.py | 236 +++++++++++++++++++++++++++- src/buildcompiler/constants.py | 1 + src/buildcompiler/robotutils.py | 241 +++++++++++++++++++++++++++++ tests/test_plating.py | 130 ++++++++++++++++ 5 files changed, 612 insertions(+), 1 deletion(-) create mode 100644 tests/test_plating.py diff --git a/pyproject.toml b/pyproject.toml index 7130b71..a0c13bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,11 @@ test = [ "pytest < 5.0.0", "pytest-cov[all]" ] +automation = [ + "pudupy", + "opentrons", + "SBOLInventory @ git+https://github.com/DRAGGON-Lab/SBOLInventory.git ; python_version >= '3.10'", +] [project.urls] "Homepage" = "https://github.com/MyersResearchGroup/BuildCompiler" diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index aa0511b..7e4005f 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -1,6 +1,8 @@ import sbol2 import random import warnings +import urllib.parse +from pathlib import Path from typing import Any, Dict, List from buildcompiler.plasmid import Plasmid @@ -9,7 +11,16 @@ get_or_pull, get_compatible_plasmids, ) -from .robotutils import assembly_plan_RDF_to_JSON +from .robotutils import ( + assembly_plan_RDF_to_JSON, + generate_96_well_positions, + normalize_plating_input, + run_opentrons_script_to_zip, + write_manual_plating_protocol, + write_plate_map_csv, + write_plate_map_json, + write_plating_protocol_script, +) from .constants import ( AMP, KAN, @@ -21,6 +32,7 @@ ENGINEERED_PLASMID, PLASMID_CLONING_VECTOR, ORGANISM_STRAIN, + PLATING_ACTIVITY_ROLE, ) @@ -502,6 +514,228 @@ def transformation( }, } + def plating( + self, + transformation_results: dict, + results_dir: str | Path, + protocol_type: str = "manual", + advanced_params: dict | None = None, + plate_name: str | None = None, + plating_doc: sbol2.Document | None = None, + overwrite: bool = False, + ) -> Dict[str, Any]: + """Generate a plated 96-well output and protocol artifacts.""" + if protocol_type not in {"manual", "automated"}: + raise ValueError("protocol_type must be one of: 'manual', 'automated'.") + if plating_doc is None: + plating_doc = self.sbol_doc + advanced_params = advanced_params or {} + + normalized = normalize_plating_input(transformation_results, doc=plating_doc) + if len(normalized) > 96: + raise ValueError("plating supports up to 96 transformed strains.") + + wells = generate_96_well_positions(limit=len(normalized)) + results_path = Path(results_dir) + results_path.mkdir(parents=True, exist_ok=True) + + plate_id = plate_name or "solid_96_well_plate" + plate_impl = sbol2.Implementation(plate_id) + plate_md = plating_doc.find("solid_96_well_plate_md") or sbol2.ModuleDefinition( + "solid_96_well_plate_md" + ) + plate_md.name = "Solid 96-well plate" + self._add_if_absent(plating_doc, plate_md) + plate_impl.built = plate_md.identity + self._add_if_absent(plating_doc, plate_impl) + + # Optional SBOLInventory integration with fallback behavior. + try: + from sbol_inventory import ( # type: ignore + make_solid_96_well_plate, + make_plated_strain, + place_in_plate, + ) + + inventory_enabled = True + inventory_plate = make_solid_96_well_plate( + uri=plate_impl.identity, plate_md_uri=plate_md.identity + ) + except Exception: + inventory_enabled = False + inventory_plate = None + + activity_id = f"plating_{protocol_type}_{plate_id}" + plating_activity = sbol2.Activity(activity_id) + plating_activity.name = f"Plating activity for {plate_id}" + plating_activity.types = "http://sbols.org/v2#build" + self._add_if_absent(plating_doc, plating_activity) + + agent_id = ( + "manual_plating_agent" + if protocol_type == "manual" + else "opentrons_plating_agent" + ) + agent = plating_doc.find(agent_id) or sbol2.Agent(agent_id) + agent.name = "Manual plating agent" if protocol_type == "manual" else "Opentrons plating agent" + self._add_if_absent(plating_doc, agent) + + plan_id = f"{plate_id}_{protocol_type}_plating_plan" + plan = plating_doc.find(plan_id) or sbol2.Plan(plan_id) + plan.name = f"{protocol_type.title()} plating plan for {plate_id}" + self._add_if_absent(plating_doc, plan) + + association = sbol2.Association( + uri=f"{activity_id}_association", + agent=agent.identity, + role="http://sbols.org/v2#build", + ) + association.plan = plan.identity + plating_activity.associations = [association] + self._add_if_absent(plating_doc, association) + + plate_rows = [] + plate_map = {} + bacterium_locations = {} + plated_impls = [] + + for idx, entry in enumerate(normalized): + well = wells[idx] + source_impl_uri = entry.get("source_impl_uri") + source_impl = plating_doc.find(source_impl_uri) if source_impl_uri else None + strain_module_uri = entry.get("strain_module_uri") + if strain_module_uri is None and source_impl is not None: + strain_module_uri = getattr(source_impl, "built", None) + + display_source = source_impl_uri or strain_module_uri or f"strain_{idx+1}" + parsed = urllib.parse.urlparse(display_source) + slug = parsed.path.split("/")[-1] if parsed.path else display_source + slug = slug.replace("#", "_").replace(":", "_") + + plated_module_id = f"{slug}_plated_{well}_md" + plated_module = plating_doc.find(plated_module_id) or sbol2.ModuleDefinition( + plated_module_id + ) + plated_module.roles = [ORGANISM_STRAIN] + plated_module.name = f"Plated strain {slug} at {well}" + if strain_module_uri: + plated_module.wasDerivedFrom = strain_module_uri + self._add_if_absent(plating_doc, plated_module) + + plated_impl_id = f"{slug}_plated_{well}_impl" + plated_impl = plating_doc.find(plated_impl_id) or sbol2.Implementation( + plated_impl_id + ) + plated_impl.built = plated_module.identity + plated_impl.wasGeneratedBy = plating_activity.identity + if source_impl_uri: + plated_impl.wasDerivedFrom = source_impl_uri + self._add_if_absent(plating_doc, plated_impl) + plated_impls.append(plated_impl.identity) + + usage = sbol2.Usage( + uri=f"{activity_id}_usage_{idx+1}", + entity=source_impl_uri or plated_module.identity, + role=PLATING_ACTIVITY_ROLE, + ) + self._add_if_absent(plating_doc, usage) + current_usages = list(plating_activity.usages) + current_usages.append(usage) + plating_activity.usages = current_usages + + if inventory_enabled: + try: + inventory_plated = make_plated_strain( + uri=plated_impl.identity, + strain_md_uri=strain_module_uri or plated_module.identity, + design_uri=source_impl_uri, + ) + place_in_plate(inventory_plate, inventory_plated, well) + except Exception: + inventory_enabled = False + + plate_map[well] = plated_impl.identity + display_name = plated_module.displayId + bacterium_locations[well] = display_name + plate_rows.append( + { + "well": well, + "source_transformed_strain_implementation": source_impl_uri, + "strain_module": strain_module_uri, + "plated_strain_implementation": plated_impl.identity, + "strain_display_name": display_name, + } + ) + + plate_map_json_path = write_plate_map_json( + results_path / "plate_map.json", + { + "plate_implementation": plate_impl.identity, + "protocol_type": protocol_type, + "well_map": plate_rows, + }, + ) + plate_map_csv_path = write_plate_map_csv(results_path / "plate_map.csv", plate_rows) + plating_input_json_path = write_plate_map_json( + results_path / "plating_input.json", + {"bacterium_locations": bacterium_locations}, + ) + + logs = [] + protocol_artifacts: Dict[str, Any] = { + "plate_map_json": str(plate_map_json_path), + "plate_map_csv": str(plate_map_csv_path), + "logs": logs, + } + + if protocol_type == "manual": + md_path = write_manual_plating_protocol( + results_path / "manual_plating_protocol.md", + plate_id=plate_impl.displayId, + plate_rows=plate_rows, + advanced_params=advanced_params, + ) + protocol_artifacts["manual_protocol_markdown"] = str(md_path) + plan.description = f"Manual protocol file: {md_path}" + else: + script_path = write_plating_protocol_script( + results_path / "plating_ot2.py", + plating_data={"bacterium_locations": bacterium_locations}, + advanced_params=advanced_params, + ) + protocol_artifacts["ot2_script"] = str(script_path) + plan.description = f"Automated protocol script: {script_path}" + try: + sim_zip = run_opentrons_script_to_zip( + script_path, + plating_input_json_path, + overwrite=overwrite, + ) + protocol_artifacts["simulation_zip"] = str(sim_zip) + except Exception as exc: + logs.append(f"Opentrons simulation skipped: {exc}") + + return { + "stage": "plating", + "protocol_type": protocol_type, + "plate": { + "plate_implementation": plate_impl.identity, + "plate_map": plate_map, + }, + "sbol_artifacts": { + "plating_activity": plating_activity.identity, + "agent": agent.identity, + "plan": plan.identity, + "plate_implementation": plate_impl.identity, + "plated_strain_implementations": plated_impls, + }, + "json_intermediate": { + "plating_data": {"bacterium_locations": bacterium_locations}, + "advanced_params": advanced_params, + }, + "protocol_artifacts": protocol_artifacts, + } + def _extract_plasmids_from_strain( self, strain: sbol2.ModuleDefinition, diff --git a/src/buildcompiler/constants.py b/src/buildcompiler/constants.py index 19c25a2..1fed16a 100644 --- a/src/buildcompiler/constants.py +++ b/src/buildcompiler/constants.py @@ -35,6 +35,7 @@ RESTRICTION_ENZYME = "http://identifiers.org/obi/OBI_0000732" RESTRICTION_ENZYME_ASSEMBLY_SCAR = "http://identifiers.org/so/SO:0001953" ORGANISM_STRAIN = "https://identifiers.org/ncit/NCIT:C14419" +PLATING_ACTIVITY_ROLE = "https://w3id.org/buildcompiler/roles/plating_input" FIVE_PRIME_OVERHANG = "http://identifiers.org/so/SO:0001932" THREE_PRIME_OVERHANG = "http://identifiers.org/so/SO:0001933" diff --git a/src/buildcompiler/robotutils.py b/src/buildcompiler/robotutils.py index 5b91cf9..6db50d6 100644 --- a/src/buildcompiler/robotutils.py +++ b/src/buildcompiler/robotutils.py @@ -1,10 +1,251 @@ import sbol2 import json +import csv import shutil import subprocess import tempfile import zipfile from pathlib import Path +from typing import Any, Dict, List + + +def load_json_or_dict(value): + """Load a JSON file path/string into a dict, or return dict-like input as-is.""" + if isinstance(value, dict): + return value + if isinstance(value, Path): + return json.loads(value.read_text(encoding="utf-8")) + if isinstance(value, str): + path = Path(value) + if path.exists(): + return json.loads(path.read_text(encoding="utf-8")) + return json.loads(value) + raise ValueError("Expected dict, JSON string, or JSON file path.") + + +def normalize_plating_input(transformation_results, doc=None): + """Normalize transformation/plating inputs to a deterministic list of entries.""" + payload = load_json_or_dict(transformation_results) + normalized = [] + + if isinstance(payload, dict) and isinstance(payload.get("sbol_artifacts"), list): + for idx, artifact in enumerate(payload["sbol_artifacts"], start=1): + if not isinstance(artifact, dict): + continue + impl_uri = artifact.get("transformed_strain_implementation") + module_uri = artifact.get("transformed_strain_module") + if not impl_uri and not module_uri: + continue + normalized.append( + { + "order": idx, + "well_hint": None, + "source_impl_uri": impl_uri, + "strain_module_uri": module_uri, + } + ) + elif isinstance(payload, dict) and isinstance(payload.get("strain_locations"), dict): + for idx, well in enumerate(sorted(payload["strain_locations"]), start=1): + impl_uri = payload["strain_locations"][well] + normalized.append( + { + "order": idx, + "well_hint": well, + "source_impl_uri": impl_uri, + "strain_module_uri": None, + } + ) + elif isinstance(payload, dict) and isinstance( + payload.get("bacterium_locations"), dict + ): + for idx, well in enumerate(sorted(payload["bacterium_locations"]), start=1): + impl_uri = payload["bacterium_locations"][well] + normalized.append( + { + "order": idx, + "well_hint": well, + "source_impl_uri": impl_uri, + "strain_module_uri": None, + } + ) + else: + raise ValueError( + "Unsupported plating input shape. Expected transformation output, " + "strain_locations, or bacterium_locations." + ) + + if doc is not None: + for item in normalized: + if item["strain_module_uri"] is None and item["source_impl_uri"]: + impl = doc.find(item["source_impl_uri"]) + if impl is not None and getattr(impl, "built", None): + item["strain_module_uri"] = impl.built + + if not normalized: + raise ValueError("No transformed strains found in plating input.") + + return sorted(normalized, key=lambda x: x["order"]) + + +def generate_96_well_positions(limit=96): + wells = [f"{row}{column}" for row in "ABCDEFGH" for column in range(1, 13)] + if limit < 0: + raise ValueError("limit must be non-negative") + return wells[:limit] + + +def write_plate_map_json(path, data): + path = Path(path) + path.write_text(json.dumps(data, indent=2), encoding="utf-8") + return path + + +def write_plate_map_csv(path, rows: List[Dict[str, Any]]): + path = Path(path) + fields = [ + "well", + "source_transformed_strain_implementation", + "strain_module", + "plated_strain_implementation", + "strain_display_name", + ] + with path.open("w", newline="", encoding="utf-8") as handle: + writer = csv.DictWriter(handle, fieldnames=fields) + writer.writeheader() + for row in rows: + writer.writerow( + { + "well": row.get("well"), + "source_transformed_strain_implementation": row.get( + "source_transformed_strain_implementation" + ), + "strain_module": row.get("strain_module"), + "plated_strain_implementation": row.get( + "plated_strain_implementation" + ), + "strain_display_name": row.get("strain_display_name"), + } + ) + return path + + +def write_manual_plating_protocol(path, plate_id, plate_rows, advanced_params): + path = Path(path) + params = advanced_params or {} + table_lines = [ + "| Well | Source transformed strain implementation | Plated strain implementation | Strain module |", + "|---|---|---|---|", + ] + for row in plate_rows: + table_lines.append( + f"| {row['well']} | {row.get('source_transformed_strain_implementation','')} " + f"| {row.get('plated_strain_implementation','')} | {row.get('strain_module','')} |" + ) + + param_lines = "\n".join([f"- **{k}**: {v}" for k, v in sorted(params.items())]) or "- (none)" + strain_lines = "\n".join( + [ + f"- {row.get('strain_display_name', row.get('source_transformed_strain_implementation'))}" + for row in plate_rows + ] + ) + content = ( + "# BuildCompiler Plating Protocol\n\n" + f"## Plate\n- Plate ID: `{plate_id}`\n- Protocol type: `manual`\n\n" + "## Input transformed strains\n" + f"{strain_lines}\n\n" + "## Parameters\n" + f"{param_lines}\n\n" + "## 96-well plate map\n" + f"{chr(10).join(table_lines)}\n\n" + "## Steps\n" + "1. Prepare one sterile solid-media 96-well plate.\n" + "2. Label the plate with the plate ID and date.\n" + "3. Transfer each transformed strain to the destination well shown in the map.\n" + "4. Incubate according to lab defaults or parameters above.\n" + ) + path.write_text(content, encoding="utf-8") + return path + + +def write_plating_protocol_script(path, plating_data, advanced_params): + path = Path(path) + script = ( + "from pudu.plating import Plating\n" + "from opentrons import protocol_api\n\n" + "metadata = {\n" + ' "protocolName": "BuildCompiler Plating",\n' + ' "author": "BuildCompiler",\n' + ' "description": "Automated plating protocol generated from BuildCompiler transformation results",\n' + ' "apiLevel": "2.21",\n' + "}\n\n" + f"PLATING_DATA = {json.dumps(plating_data, indent=4)}\n" + f"ADVANCED_PARAMS = {json.dumps(advanced_params or {}, indent=4)}\n\n" + "def run(protocol: protocol_api.ProtocolContext):\n" + " plating = Plating(\n" + " plating_data=PLATING_DATA,\n" + " json_params=ADVANCED_PARAMS,\n" + " )\n" + " plating.run(protocol)\n" + ) + path.write_text(script, encoding="utf-8") + return path + + +def run_opentrons_script_to_zip( + opentrons_script_path, + plating_json_path, + zip_name=None, + overwrite=False, +): + script_path = Path(opentrons_script_path).resolve() + json_path = Path(plating_json_path).resolve() + if not script_path.exists(): + raise FileNotFoundError(f"Opentrons script not found: {script_path}") + if not json_path.exists(): + raise FileNotFoundError(f"JSON file not found: {json_path}") + + out_zip = script_path.parent / (zip_name or f"{script_path.stem}_simulation.zip") + if out_zip.exists() and not overwrite: + stem = out_zip.stem + suffix = out_zip.suffix + i = 1 + while True: + candidate = out_zip.parent / f"{stem}_{i}{suffix}" + if not candidate.exists(): + out_zip = candidate + break + i += 1 + + with tempfile.TemporaryDirectory() as tmpdirname: + tmpdir = Path(tmpdirname) + tmp_script = tmpdir / script_path.name + tmp_json = tmpdir / json_path.name + shutil.copy2(script_path, tmp_script) + shutil.copy2(json_path, tmp_json) + + proc = subprocess.run( + ["opentrons_simulate", str(tmp_script)], + capture_output=True, + text=True, + cwd=tmpdir, + ) + (tmpdir / "simulate_stdout.txt").write_text( + proc.stdout or "", encoding="utf-8", errors="replace" + ) + (tmpdir / "simulate_stderr.txt").write_text( + proc.stderr or "", encoding="utf-8", errors="replace" + ) + (tmpdir / "simulate_returncode.txt").write_text( + str(proc.returncode), encoding="utf-8" + ) + + with zipfile.ZipFile(out_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf: + for file_path in tmpdir.rglob("*"): + if file_path.is_file(): + zf.write(file_path, arcname=file_path.relative_to(tmpdir)) + + return out_zip def assembly_plan_RDF_to_JSON(file, output_path: str | Path | None = None): if isinstance(file, sbol2.Document): diff --git a/tests/test_plating.py b/tests/test_plating.py new file mode 100644 index 0000000..e0c1684 --- /dev/null +++ b/tests/test_plating.py @@ -0,0 +1,130 @@ +import os +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +import sbol2 + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +from buildcompiler.buildcompiler import BuildCompiler +from buildcompiler.constants import ENGINEERED_PLASMID, ORGANISM_STRAIN +from buildcompiler.robotutils import generate_96_well_positions, normalize_plating_input + + +class _ProcResult: + def __init__(self, returncode=0, stdout="", stderr=""): + self.returncode = returncode + self.stdout = stdout + self.stderr = stderr + + +class TestPlating(unittest.TestCase): + def setUp(self): + self.doc = sbol2.Document() + self.compiler = BuildCompiler( + collections=[], + sbh_registry="https://synbiohub.org", + auth_token="", + sbol_doc=self.doc, + ) + self._id_seed = 0 + + def _make_transform_result(self, count=1): + artifacts = [] + self._id_seed += 1 + prefix = f"t{self._id_seed}_" + for i in range(1, count + 1): + plasmid = sbol2.ComponentDefinition(f"{prefix}p{i}") + plasmid.roles = [ENGINEERED_PLASMID] + self.doc.add(plasmid) + transform_module = sbol2.ModuleDefinition(f"{prefix}strain_{i}") + transform_module.roles = [ORGANISM_STRAIN] + self.doc.add(transform_module) + impl = sbol2.Implementation(f"{prefix}strain_{i}_impl") + impl.built = transform_module.identity + self.doc.add(impl) + artifacts.append( + { + "transformed_strain_module": transform_module.identity, + "transformed_strain_implementation": impl.identity, + } + ) + return {"stage": "transformation", "sbol_artifacts": artifacts} + + def test_normalization_shapes(self): + result = self._make_transform_result(2) + normalized = normalize_plating_input(result, doc=self.doc) + self.assertEqual(len(normalized), 2) + + sloc = normalize_plating_input({"strain_locations": {"A1": "x", "A2": "y"}}) + self.assertEqual(len(sloc), 2) + + bloc = normalize_plating_input({"bacterium_locations": {"A1": "x"}}) + self.assertEqual(len(bloc), 1) + + with self.assertRaises(ValueError): + normalize_plating_input({"invalid": True}) + + def test_plate_well_mapping_limits(self): + self.assertEqual(generate_96_well_positions(1), ["A1"]) + self.assertEqual(generate_96_well_positions(13)[-1], "B1") + self.assertEqual(len(generate_96_well_positions(96)), 96) + + with tempfile.TemporaryDirectory() as tmpdir: + ok = self.compiler.plating( + self._make_transform_result(96), Path(tmpdir), protocol_type="manual" + ) + self.assertEqual(len(ok["plate"]["plate_map"]), 96) + + with self.assertRaises(ValueError): + self.compiler.plating( + self._make_transform_result(97), Path(tmpdir), protocol_type="manual" + ) + + def test_plating_manual_outputs_and_provenance(self): + with tempfile.TemporaryDirectory() as tmpdir: + result = self.compiler.plating( + transformation_results=self._make_transform_result(2), + results_dir=tmpdir, + protocol_type="manual", + advanced_params={"incubation_temperature_c": 37}, + ) + self.assertEqual(result["stage"], "plating") + self.assertEqual(result["protocol_type"], "manual") + self.assertEqual(len(result["plate"]["plate_map"]), 2) + self.assertTrue( + Path(result["protocol_artifacts"]["manual_protocol_markdown"]).exists() + ) + self.assertTrue(Path(result["protocol_artifacts"]["plate_map_json"]).exists()) + self.assertTrue(Path(result["protocol_artifacts"]["plate_map_csv"]).exists()) + + activity = self.doc.find(result["sbol_artifacts"]["plating_activity"]) + self.assertIsNotNone(activity) + self.assertEqual(len(activity.usages), 2) + self.assertTrue(len(activity.associations) >= 1) + + @patch("buildcompiler.robotutils.subprocess.run") + def test_plating_automated_script_and_sim_zip(self, mock_run): + mock_run.return_value = _ProcResult(returncode=0, stdout="ok", stderr="") + with tempfile.TemporaryDirectory() as tmpdir: + result = self.compiler.plating( + transformation_results=self._make_transform_result(1), + results_dir=tmpdir, + protocol_type="automated", + advanced_params={"replicates": 1}, + overwrite=True, + ) + script_path = Path(result["protocol_artifacts"]["ot2_script"]) + script = script_path.read_text(encoding="utf-8") + self.assertIn("def run(protocol: protocol_api.ProtocolContext):", script) + self.assertIn("from opentrons import protocol_api", script) + self.assertIn("json_params=ADVANCED_PARAMS", script) + self.assertNotIn("advanced_params=ADVANCED_PARAMS", script) + self.assertTrue(Path(result["protocol_artifacts"]["simulation_zip"]).exists()) + + +if __name__ == "__main__": + unittest.main() From c75a283bbe821e22cfc891a526d788c80666e72e Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 28 Apr 2026 00:26:13 -0600 Subject: [PATCH 29/93] Implement full_build orchestration with manifest and packaging --- src/buildcompiler/buildcompiler.py | 387 +++++++++++++++++++++++++++++ tests/test_full_build.py | 178 +++++++++++++ 2 files changed, 565 insertions(+) create mode 100644 tests/test_full_build.py diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index 7e4005f..21fbea9 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -1,5 +1,8 @@ import sbol2 +import json import random +import re +import shutil import warnings import urllib.parse from pathlib import Path @@ -8,6 +11,8 @@ from buildcompiler.plasmid import Plasmid from buildcompiler.sbol2build import Assembly, dna_componentdefinition_with_sequence from .abstract_translator import ( + enumerate_design_variants, + extract_combinatorial_design_parts, get_or_pull, get_compatible_plasmids, ) @@ -1012,6 +1017,388 @@ def _normalize_transformation_inputs( ) return normalized + def _safe_display_id(self, value: str) -> str: + safe_value = re.sub(r"[^A-Za-z0-9_]+", "_", value or "") + return safe_value.strip("_") or "unnamed_design" + + def _serialize_sbol_identity(self, obj_or_uri) -> str: + return getattr(obj_or_uri, "identity", str(obj_or_uri)) + + def _write_json(self, path: Path, payload: dict) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8") as handle: + json.dump(payload, handle, indent=2, sort_keys=True) + return path + + def _status_from_manifest(self, manifest: dict) -> str: + if manifest.get("errors"): + return "completed_with_errors" + if manifest["assembly_lvl1"].get("successful"): + return "completed" + if ( + manifest["assembly_lvl1"].get("failed") + or manifest["domestication"].get("errors") + ): + return "completed_with_errors" + return "failed" + + def _find_missing_parts_for_lvl1( + self, + design: sbol2.ComponentDefinition, + backbone: Plasmid = None, + ) -> list[dict]: + parts = self._extract_design_parts(design) + plasmid_dict = self._construct_plasmid_dict(parts, antibiotic_resistance=AMP) + + missing_parts = [] + for part in parts: + candidates = plasmid_dict.get(part.displayId, []) + if not candidates: + missing_parts.append( + { + "part": part, + "reason": "no implemented plasmid", + } + ) + continue + + try: + if backbone is None: + selected_backbone, _ = self._get_backbone( + plasmid_dict, antibiotic_resistance=KAN + ) + if selected_backbone is None: + missing_parts.append( + { + "part": part, + "reason": "no compatible backbone", + } + ) + else: + compatible = get_compatible_plasmids(plasmid_dict, backbone) + if not compatible: + missing_parts.append( + { + "part": part, + "reason": "no compatible plasmid", + } + ) + except Exception: + missing_parts.append( + { + "part": part, + "reason": "no compatible plasmid", + } + ) + + return missing_parts + + def _index_domestication_products( + self, + products: list[sbol2.ComponentDefinition], + ) -> None: + for product_definition in products: + if self._get_indexed_plasmid(self.indexed_plasmids, product_definition): + continue + try: + indexed = Plasmid(product_definition, None, [], [], self.sbol_doc) + except Exception: + indexed = type( + "IndexedDomesticationPlasmid", + (), + { + "plasmid_definition": product_definition, + "name": product_definition.displayId, + "fusion_sites": [], + "antibiotic_resistance": None, + }, + )() + self.indexed_plasmids.append(indexed) + + def _zip_full_build_results( + self, + source_dir: Path, + zip_path: Path, + overwrite: bool = False, + ) -> Path: + if zip_path.exists(): + if not overwrite: + raise FileExistsError( + f"Zip path already exists and overwrite=False: {zip_path}" + ) + zip_path.unlink() + zip_path.parent.mkdir(parents=True, exist_ok=True) + zip_base = zip_path.with_suffix("") + archive = shutil.make_archive( + str(zip_base), "zip", root_dir=str(source_dir), base_dir="." + ) + return Path(archive) + + def _expand_combinatorial_derivation( + self, + derivation: sbol2.CombinatorialDerivation, + product_name_prefix: str = None, + ) -> list[sbol2.ComponentDefinition]: + master_template = get_or_pull(self.sbol_doc, self.sbh, derivation.masterTemplate) + component_variants = extract_combinatorial_design_parts( + master_template, self.sbol_doc, self.sbol_doc + ) + variant_definitions = enumerate_design_variants(component_variants) + + prefix = product_name_prefix or master_template.displayId + created_variants = [] + + ordered_components = list(master_template.getInSequentialOrder()) + for index, variant_parts in enumerate(variant_definitions, start=1): + variant_display_id = f"{self._safe_display_id(prefix)}_variant_{index:03d}" + variant_design = self.sbol_doc.find(variant_display_id) or sbol2.ComponentDefinition( + variant_display_id + ) + variant_design.types = list(master_template.types) + variant_design.roles = list(master_template.roles) + variant_design.wasDerivedFrom = derivation.identity + self._add_if_absent(self.sbol_doc, variant_design) + + if len(variant_design.components) == 0: + for comp_index, component in enumerate(ordered_components): + part_def = variant_parts[comp_index] + variant_component = variant_design.components.create( + f"{variant_display_id}_component_{comp_index+1:03d}" + ) + variant_component.definition = part_def.identity + try: + variant_component.access = component.access + except Exception: + pass + try: + variant_component.direction = component.direction + except Exception: + pass + created_variants.append(variant_design) + + return created_variants + + def _normalize_full_build_designs(self, designs) -> list[sbol2.ComponentDefinition]: + if isinstance(designs, sbol2.ComponentDefinition): + return [designs] + if isinstance(designs, sbol2.CombinatorialDerivation): + return self._expand_combinatorial_derivation(designs) + if isinstance(designs, list): + if all(isinstance(design, sbol2.ComponentDefinition) for design in designs): + return designs + raise TypeError("designs list must contain only ComponentDefinition objects.") + raise TypeError( + "designs must be a ComponentDefinition, list[ComponentDefinition], or CombinatorialDerivation." + ) + + def full_build( + self, + designs, + results_dir, + chassis_name: str = "E_coli_DH5alpha", + protocol_type: str = "manual", + transformation_params: dict | None = None, + plating_params: dict | None = None, + product_name_prefix: str | None = None, + overwrite: bool = False, + ) -> dict: + transformation_params = transformation_params or {} + plating_params = plating_params or {} + results_path = Path(results_dir) + results_path.mkdir(parents=True, exist_ok=True) + + input_type = type(designs).__name__ + normalized_designs = self._normalize_full_build_designs(designs) + manifest = { + "stage": "full_build", + "inputs": { + "input_type": input_type, + "design_count": len(normalized_designs), + "chassis_name": chassis_name, + "protocol_type": protocol_type, + "product_name_prefix": product_name_prefix, + }, + "domestication": { + "missing_parts": [], + "products": [], + "transformation": {}, + "plating": {}, + "errors": [], + }, + "assembly_lvl1": {"successful": [], "failed": []}, + "transformation": {"assembly_products": {}}, + "plating": {"assembly_products": {}}, + "skipped": [ + { + "stage": "assembly_lvl2", + "status": "skipped", + "reason": "assembly_lvl2 is not implemented yet", + } + ], + "errors": [], + } + + per_design_missing = {} + unique_missing = {} + for design in normalized_designs: + missing_items = self._find_missing_parts_for_lvl1(design) + serialized_missing = [] + for item in missing_items: + part = item["part"] + entry = { + "part_identity": self._serialize_sbol_identity(part), + "part_display_id": part.displayId, + "reason": item["reason"], + } + serialized_missing.append(entry) + unique_missing.setdefault(part.identity, {"part": part, "reason": item["reason"]}) + per_design_missing[design.identity] = serialized_missing + + manifest["domestication"]["missing_parts"] = list(unique_missing.values()) + if unique_missing: + manifest["domestication"]["missing_parts"] = [ + { + "part_identity": self._serialize_sbol_identity(item["part"]), + "part_display_id": item["part"].displayId, + "reason": item["reason"], + } + for item in unique_missing.values() + ] + unique_missing_parts = [item["part"] for item in unique_missing.values()] + try: + domesticated_products = self.domestication(unique_missing_parts) + self._index_domestication_products(domesticated_products) + manifest["domestication"]["products"] = [ + self._serialize_sbol_identity(product) + for product in domesticated_products + ] + try: + domestication_transformation = self.transformation( + domesticated_products, + chassis_name=chassis_name, + transformation_doc=self.sbol_doc, + **transformation_params, + ) + manifest["domestication"]["transformation"] = domestication_transformation + except Exception as exc: + manifest["domestication"]["errors"].append( + f"Domestication transformation failed: {exc}" + ) + domestication_transformation = None + + if domestication_transformation: + try: + domestication_plating = self.plating( + transformation_results=domestication_transformation, + results_dir=results_path / "domestication" / "plating", + protocol_type=protocol_type, + advanced_params=plating_params, + plating_doc=self.sbol_doc, + overwrite=overwrite, + ) + manifest["domestication"]["plating"] = domestication_plating + except Exception as exc: + manifest["domestication"]["errors"].append( + f"Domestication plating failed: {exc}" + ) + except Exception as exc: + manifest["domestication"]["errors"].append(f"Domestication failed: {exc}") + manifest["errors"].append(f"Domestication failed: {exc}") + + for index, design in enumerate(normalized_designs, start=1): + design_slug = self._safe_display_id(design.displayId or f"design_{index:03d}") + stable_product_name = ( + f"{self._safe_display_id(product_name_prefix)}_{design_slug}_{index:03d}" + if product_name_prefix + else f"{design_slug}_{index:03d}" + ) + try: + assembly_products = self.assembly_lvl1( + abstract_design=design, + product_name=stable_product_name, + ) + product_ids = [ + self._serialize_sbol_identity( + product.plasmid_definition if isinstance(product, Plasmid) else product + ) + for product in assembly_products + ] + + assembly_transformation = self.transformation( + assembly_products, + chassis_name=chassis_name, + transformation_doc=self.sbol_doc, + **transformation_params, + ) + assembly_plating = self.plating( + transformation_results=assembly_transformation, + results_dir=results_path / "assembly_lvl1" / design_slug / "plating", + protocol_type=protocol_type, + advanced_params=plating_params, + plating_doc=self.sbol_doc, + overwrite=overwrite, + ) + + manifest["assembly_lvl1"]["successful"].append( + { + "design_identity": design.identity, + "design_display_id": design.displayId, + "assembly_product_identities": product_ids, + } + ) + manifest["transformation"]["assembly_products"][design_slug] = assembly_transformation + manifest["plating"]["assembly_products"][design_slug] = assembly_plating + except Exception as exc: + failure_entry = { + "design_identity": design.identity, + "design_display_id": design.displayId, + "error": str(exc), + "missing_parts": per_design_missing.get(design.identity, []), + } + manifest["assembly_lvl1"]["failed"].append(failure_entry) + manifest["errors"].append( + f"Assembly failed for {design.displayId}: {exc}" + ) + + sbol_path = results_path / "sbol" / "full_build.xml" + try: + sbol_path.parent.mkdir(parents=True, exist_ok=True) + self.sbol_doc.write(str(sbol_path)) + except Exception as exc: + manifest["errors"].append(f"SBOL write failed: {exc}") + + manifest_path = self._write_json(results_path / "full_build_manifest.json", manifest) + zip_path = results_path / "full_build_results.zip" + try: + zip_result = self._zip_full_build_results( + source_dir=results_path, + zip_path=zip_path, + overwrite=overwrite, + ) + except Exception as exc: + manifest["errors"].append(f"Result packaging failed: {exc}") + self._write_json(manifest_path, manifest) + zip_result = zip_path + + status = self._status_from_manifest(manifest) + result = { + "stage": "full_build", + "status": status, + "results_dir": str(results_path), + "zip_path": str(zip_result), + "manifest_path": str(manifest_path), + "sbol_path": str(sbol_path), + "inputs": manifest["inputs"], + "domestication": manifest["domestication"], + "assembly_lvl1": manifest["assembly_lvl1"], + "transformation": manifest["transformation"], + "plating": manifest["plating"], + "skipped": manifest["skipped"], + "errors": manifest["errors"], + } + self._write_json(manifest_path, manifest) + return result + def _get_or_create_chassis( self, doc: sbol2.Document, chassis_name: str ) -> tuple[sbol2.ModuleDefinition, sbol2.Implementation]: diff --git a/tests/test_full_build.py b/tests/test_full_build.py new file mode 100644 index 0000000..eb3a532 --- /dev/null +++ b/tests/test_full_build.py @@ -0,0 +1,178 @@ +import os +import sys +import tempfile +import unittest +import zipfile +from pathlib import Path +from unittest.mock import MagicMock, patch + +import sbol2 + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +from buildcompiler.buildcompiler import BuildCompiler +from buildcompiler.constants import ENGINEERED_PLASMID + + +class TestFullBuild(unittest.TestCase): + def setUp(self): + self.doc = sbol2.Document() + self.compiler = BuildCompiler( + collections=[], + sbh_registry="https://synbiohub.org", + auth_token="", + sbol_doc=self.doc, + ) + + def _make_part(self, display_id: str) -> sbol2.ComponentDefinition: + part = sbol2.ComponentDefinition(display_id) + self.doc.add(part) + return part + + def _get_by_display_id(self, display_id: str): + for obj in self.doc.componentDefinitions: + if obj.displayId == display_id: + return obj + return None + + def _make_design(self, display_id: str, part_ids: list[str]) -> sbol2.ComponentDefinition: + design = sbol2.ComponentDefinition(display_id) + self.doc.add(design) + created_components = [] + for index, part_id in enumerate(part_ids, start=1): + part = self._get_by_display_id(part_id) or self._make_part(part_id) + comp = design.components.create(f"{display_id}_c{index}") + comp.definition = part.identity + created_components.append(comp) + for index in range(len(created_components) - 1): + sc = design.sequenceConstraints.create(f"{display_id}_sc{index+1}") + sc.subject = created_components[index].identity + sc.object = created_components[index + 1].identity + sc.restriction = sbol2.SBOL_RESTRICTION_PRECEDES + return design + + def _make_plasmid(self, display_id: str) -> sbol2.ComponentDefinition: + plasmid = sbol2.ComponentDefinition(display_id) + plasmid.roles = [ENGINEERED_PLASMID] + self.doc.add(plasmid) + return plasmid + + def test_normalize_full_build_designs_input_shapes(self): + d1 = self._make_design("design_a", ["part_a"]) + d2 = self._make_design("design_b", ["part_b"]) + + self.assertEqual(self.compiler._normalize_full_build_designs(d1), [d1]) + self.assertEqual(self.compiler._normalize_full_build_designs([d1, d2]), [d1, d2]) + + derivation = sbol2.CombinatorialDerivation("combo") + self.doc.add(derivation) + with patch.object(self.compiler, "_expand_combinatorial_derivation", return_value=[d1]) as mock_expand: + normalized = self.compiler._normalize_full_build_designs(derivation) + self.assertEqual(normalized, [d1]) + mock_expand.assert_called_once_with(derivation) + + def test_expand_combinatorial_derivation_creates_deterministic_variants(self): + p1 = self._make_part("p1") + p2 = self._make_part("p2") + p3 = self._make_part("p3") + + template = self._make_design("master", ["p1", "p2"]) + derivation = sbol2.CombinatorialDerivation("combo_2") + derivation.masterTemplate = template.identity + + with patch("buildcompiler.buildcompiler.get_or_pull", return_value=template), patch( + "buildcompiler.buildcompiler.extract_combinatorial_design_parts", return_value={"a": [p1, p2], "b": [p3]} + ), patch( + "buildcompiler.buildcompiler.enumerate_design_variants", + return_value=[[p1, p3], [p2, p3]], + ): + variants = self.compiler._expand_combinatorial_derivation(derivation, product_name_prefix="combo") + + self.assertEqual([v.displayId for v in variants], ["combo_variant_001", "combo_variant_002"]) + self.assertIsNotNone(self._get_by_display_id("combo_variant_001")) + self.assertIsNotNone(self._get_by_display_id("combo_variant_002")) + + def test_find_missing_parts_reports_missing_and_present(self): + design = self._make_design("design_missing", ["part_x", "part_y"]) + part_x = self._get_by_display_id("part_x") + part_y = self._get_by_display_id("part_y") + + with patch.object(self.compiler, "_extract_design_parts", return_value=[part_x, part_y]), patch.object( + self.compiler, + "_construct_plasmid_dict", + return_value={"part_x": [object()], "part_y": []}, + ), patch.object(self.compiler, "_get_backbone", return_value=(object(), [object()])): + missing = self.compiler._find_missing_parts_for_lvl1(design) + + self.assertEqual(len(missing), 1) + self.assertEqual(missing[0]["part"].displayId, "part_y") + self.assertEqual(missing[0]["reason"], "no implemented plasmid") + + def test_full_build_orchestration_and_stage_skip(self): + design_a = self._make_design("dA", ["pa"]) + design_b = self._make_design("dB", ["pb"]) + missing_part = self._make_part("missing_part") + domesticated = self._make_plasmid("domesticated_missing_part") + assembled = self._make_plasmid("assembled_dA") + + with tempfile.TemporaryDirectory() as tmpdir, patch.object( + self.compiler, "_normalize_full_build_designs", return_value=[design_a, design_b] + ), patch.object( + self.compiler, + "_find_missing_parts_for_lvl1", + side_effect=[ + [{"part": missing_part, "reason": "no implemented plasmid"}], + [{"part": missing_part, "reason": "no implemented plasmid"}], + ], + ), patch.object(self.compiler, "domestication", return_value=[domesticated]) as mock_dom, patch.object( + self.compiler, "transformation", return_value={"stage": "transformation", "sbol_artifacts": []} + ) as mock_tx, patch.object( + self.compiler, "plating", return_value={"stage": "plating"} + ) as mock_plating, patch.object( + self.compiler, + "assembly_lvl1", + side_effect=[[assembled], RuntimeError("assembly fail")], + ) as mock_asm: + result = self.compiler.full_build(designs=[design_a, design_b], results_dir=tmpdir, overwrite=True) + + mock_dom.assert_called_once() + self.assertEqual(mock_asm.call_count, 2) + self.assertGreaterEqual(mock_tx.call_count, 2) + self.assertGreaterEqual(mock_plating.call_count, 2) + self.assertEqual(result["skipped"][0]["stage"], "assembly_lvl2") + self.assertEqual(len(result["assembly_lvl1"]["successful"]), 1) + self.assertEqual(len(result["assembly_lvl1"]["failed"]), 1) + + def test_full_build_writes_manifest_and_zip_and_return_shape(self): + design = self._make_design("d_main", ["p_main"]) + assembled = self._make_plasmid("assembled_main") + + with tempfile.TemporaryDirectory() as tmpdir, patch.object( + self.compiler, "_find_missing_parts_for_lvl1", return_value=[] + ), patch.object( + self.compiler, "assembly_lvl1", return_value=[assembled] + ), patch.object( + self.compiler, "transformation", return_value={"stage": "transformation", "sbol_artifacts": []} + ), patch.object( + self.compiler, "plating", return_value={"stage": "plating"} + ): + result = self.compiler.full_build(designs=[design], results_dir=Path(tmpdir) / "run", overwrite=True) + + manifest_path = Path(result["manifest_path"]) + zip_path = Path(result["zip_path"]) + + self.assertTrue(manifest_path.exists()) + self.assertTrue(zip_path.exists()) + self.assertIn("domestication", result) + self.assertIn("assembly_lvl1", result) + self.assertIn("transformation", result) + self.assertIn("plating", result) + self.assertIn("skipped", result) + + with zipfile.ZipFile(zip_path, "r") as archive: + names = archive.namelist() + self.assertIn("full_build_manifest.json", names) + + +if __name__ == "__main__": + unittest.main() From fe3e9e719823503f0cc1e833ef69b3972cc951bf Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 28 Apr 2026 08:37:01 -0600 Subject: [PATCH 30/93] Add full_build quickstart notebook example --- notebooks/full_build_quickstart.ipynb | 193 ++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 notebooks/full_build_quickstart.ipynb diff --git a/notebooks/full_build_quickstart.ipynb b/notebooks/full_build_quickstart.ipynb new file mode 100644 index 0000000..efbc33b --- /dev/null +++ b/notebooks/full_build_quickstart.ipynb @@ -0,0 +1,193 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# BuildCompiler `full_build(...)` Quickstart\n", + "\n", + "This notebook shows how to run the high-level `BuildCompiler.full_build(...)` workflow end-to-end.\n", + "\n", + "The orchestrated stages are:\n", + "1. domestication (only when missing parts are detected)\n", + "2. assembly level 1\n", + "3. transformation\n", + "4. plating\n", + "\n", + "`assembly_lvl2` is intentionally skipped for now and recorded as skipped in the output manifest." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1) Imports and setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "import sbol2\n", + "\n", + "from buildcompiler.buildcompiler import BuildCompiler" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2) Load an SBOL design document\n", + "\n", + "Replace the path below with your own SBOL file containing one or more abstract designs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "doc = sbol2.Document()\n", + "# Example: doc.read(\"path/to/your/designs.xml\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3) Construct the compiler\n", + "\n", + "Provide one or more SynBioHub collections that contain available parts/backbones/implementations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "compiler = BuildCompiler(\n", + " collections=[\n", + " # \"https://synbiohub.org/public/example_collection\",\n", + " ],\n", + " sbh_registry=\"https://synbiohub.org\",\n", + " auth_token=\"\",\n", + " sbol_doc=doc,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4) Full build with concrete designs (list of `ComponentDefinition`)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example lookup by displayId (adjust to your document)\n", + "# design_1 = doc.getComponentDefinition(\"design_1\")\n", + "# design_2 = doc.getComponentDefinition(\"design_2\")\n", + "# designs = [design_1, design_2]\n", + "\n", + "# result = compiler.full_build(\n", + "# designs=designs,\n", + "# results_dir=Path(\"results/full_build\"),\n", + "# chassis_name=\"E_coli_DH5alpha\",\n", + "# protocol_type=\"manual\",\n", + "# plating_params={\n", + "# \"incubation_temperature_c\": 37,\n", + "# \"incubation_time_h\": 16,\n", + "# },\n", + "# product_name_prefix=\"build\",\n", + "# overwrite=True,\n", + "# )\n", + "\n", + "# print(result[\"status\"])\n", + "# print(result[\"manifest_path\"])\n", + "# print(result[\"zip_path\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5) Full build with a `CombinatorialDerivation`\n", + "\n", + "Use this when your input is combinatorial. `full_build(...)` expands the derivation into concrete variants automatically." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# derivation = doc.combinatorialderivations[0]\n", + "\n", + "# combo_result = compiler.full_build(\n", + "# designs=derivation,\n", + "# results_dir=Path(\"results/full_build_combinatorial\"),\n", + "# chassis_name=\"E_coli_DH5alpha\",\n", + "# protocol_type=\"automated\",\n", + "# plating_params={\n", + "# \"replicates\": 1,\n", + "# \"number_dilutions\": 1,\n", + "# \"volume_colony\": 6,\n", + "# \"thermocycler_starting_well\": 0,\n", + "# },\n", + "# product_name_prefix=\"combo_build\",\n", + "# overwrite=True,\n", + "# )\n", + "\n", + "# print(combo_result[\"status\"])\n", + "# print(combo_result[\"manifest_path\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6) Return shape and generated files" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Top-level keys in returned result:\n", + "# stage, status, results_dir, zip_path, manifest_path, sbol_path,\n", + "# inputs, domestication, assembly_lvl1, transformation, plating, skipped, errors\n", + "\n", + "# Files written under :\n", + "# - full_build_manifest.json\n", + "# - full_build_results.zip\n", + "# - sbol/full_build.xml\n", + "# - domestication/* and assembly_lvl1/* plating/transformation artifacts" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 230f2ba133ac65f79d28dfc0c849251337ee0d1f Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 28 Apr 2026 14:58:08 -0600 Subject: [PATCH 31/93] get referencing CD in extract toplevel definition --- src/buildcompiler/abstract_translator.py | 25 +++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/buildcompiler/abstract_translator.py b/src/buildcompiler/abstract_translator.py index 5765123..c4fc666 100644 --- a/src/buildcompiler/abstract_translator.py +++ b/src/buildcompiler/abstract_translator.py @@ -187,7 +187,30 @@ def extract_combinatorial_design_parts( def extract_toplevel_definition(doc: sbol2.Document) -> sbol2.ComponentDefinition: - return doc.componentDefinitions[0] + cds = list(doc.componentDefinitions) + + # identities of definitions used as subcomponents + used_defs = set() + + for cd in cds: + for comp in cd.components: + used_defs.add(comp.definition) + + # candidates = composite designs not used inside another design + candidates = [ + cd for cd in cds if len(cd.components) > 0 and cd.identity not in used_defs + ] + + if len(candidates) == 1: + return candidates[0] + + if len(candidates) == 0: + raise ValueError("No top-level composite ComponentDefinition found") + + raise ValueError( + f"Multiple top-level ComponentDefinitions found: " + f"{[c.displayId for c in candidates]}" + ) def enumerate_design_variants(component_dict): From de292b81394c5bd5659f654f175e4b4d15ec3268 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 28 Apr 2026 14:59:55 -0600 Subject: [PATCH 32/93] ironing out iterative lvl1 --- notebooks/build_compiler_test.ipynb | 316 ++++++++++------------------ src/buildcompiler/buildcompiler.py | 16 +- 2 files changed, 122 insertions(+), 210 deletions(-) diff --git a/notebooks/build_compiler_test.ipynb b/notebooks/build_compiler_test.ipynb index caea5e6..62a0cbe 100644 --- a/notebooks/build_compiler_test.ipynb +++ b/notebooks/build_compiler_test.ipynb @@ -12,15 +12,45 @@ "from buildcompiler.abstract_translator import extract_toplevel_definition" ] }, + { + "cell_type": "markdown", + "id": "238f2456", + "metadata": {}, + "source": [ + "## Multi-Design Lvl 1 Testing:" + ] + }, { "cell_type": "code", "execution_count": 2, "id": "e60a9c84", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['i6TQvNp91', 'i0mwvNcgH']\n" + ] + } + ], "source": [ - "design_doc = sbol2.Document()\n", - "design_doc.read(\"../tests/test_files/ExampleLvl2_design.xml\")" + "design_path_list = [\n", + " \"../tests/test_files/moclo_parts_circuit.xml\",\n", + " \"../tests/test_files/mocloparts116.xml\",\n", + "]\n", + "design_defs = []\n", + "sbol_doc = sbol2.Document()\n", + "\n", + "for design in design_path_list:\n", + " temp_doc = sbol2.Document()\n", + " temp_doc.read(design)\n", + "\n", + " design_defs.append(extract_toplevel_definition(temp_doc))\n", + "\n", + " # sbol_doc.read(\"../tests/test_files/ExampleLvl2_design.xml\")\n", + "\n", + "print([design_def.displayId for design_def in design_defs])" ] }, { @@ -34,196 +64,63 @@ "output_type": "stream", "text": [ "Indexing collection: https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\n", - "Indexing collection: https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\n" + "Indexing collection: https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\n", + "['http://rebase.neb.com/rebase/enz/BsaI.html'] ['http://identifiers.org/obi/OBI_0000732']\n", + "[] ['http://identifiers.org/ncit/NCIT:C16796']\n", + "['http://rebase.neb.com/rebase/enz/BbsI.html'] ['http://identifiers.org/obi/OBI_0000732']\n", + "['http://rebase.neb.com/rebase/enz/SapI.html'] ['http://identifiers.org/obi/OBI_0000732']\n" ] } ], "source": [ - "auth = \"1812840e-aa95-4588-9dc3-2a94e0bc1ed4\"\n", + "auth = \"ca97f26e-9d33-4e38-810d-04d99f36e47c\"\n", "collections = [\n", " \"https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\",\n", " \"https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\",\n", "]\n", - "buildcompiler = BuildCompiler(collections, \"https://synbiohub.org\", auth, None)" + "buildcompiler = BuildCompiler(collections, \"https://synbiohub.org\", auth, sbol_doc)" ] }, { "cell_type": "code", "execution_count": 4, - "id": "1712f5ce", + "id": "19c5d2ff", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "https://synbiohub.org/user/Gon/Enzyme_Implementations/BbsI_impl/1 https://synbiohub.org/user/Gon/Enzyme_Implementations/BsaI_impl/1 https://synbiohub.org/user/Gon/Enzyme_Implementations/T4_Ligase_impl/1\n" + ] + } + ], "source": [ - "from buildcompiler.plasmid import Plasmid\n", - "from typing import List\n", - "from buildcompiler.constants import LVL2_FUSION_SITE_ORDER, KAN, AMP\n", - "from buildcompiler.sbol2build import Assembly\n", - "import warnings\n", - "\n", - "\n", - "def _extract_lvl2_TUs( # TODO send to misc helper file instead of buildcompiler.py?\n", - " design_doc: sbol2.Document,\n", - ") -> List[sbol2.ComponentDefinition]:\n", - " \"\"\"\n", - " Returns the component definitions of each level-1 component (TU)\n", - " in the design.\n", - "\n", - " Args:\n", - " design: :class:`sbol2.Document` containing the design.\n", - "\n", - " Returns:\n", - " A list of TU component definitions in sequential order.\n", - " \"\"\"\n", - " top_design = extract_toplevel_definition(design_doc)\n", - "\n", - " return [\n", - " design_doc.get(comp.definition) for comp in top_design.getInSequentialOrder()\n", - " ]\n", - "\n", - "\n", - "def assembly_lvl2(\n", - " self_buildcompiler,\n", - " abstract_design_doc: sbol2.Document,\n", - " backbone: Plasmid = None,\n", - " product_name: str = None,\n", - ") -> list[sbol2.ComponentDefinition]:\n", - " \"\"\"Assemble level-2 plasmids for the full design.\n", - "\n", - " Uses the assembled lvl1 plasmids and the current design to assemble\n", - " lvl2 plasmids in the correct order.\n", - "\n", - " :returns: List of assembled lvl2 plasmids.\n", - " :rtype: list[Plasmid]\n", - " :raises LookupError: If compatible plasmids or backbones cannot be found.\n", - " \"\"\"\n", - " # get high level genes, send to assembly_lvl1\n", - " # send original abstract_design to get a new dictionary\n", - " # send new dictionary to _get_backbone or get_compatible plasmids with AMP\n", - " TUs = _extract_lvl2_TUs(abstract_design_doc)\n", - " lvl1_plasmids = []\n", - "\n", - " for i, TU in enumerate(TUs):\n", - " print(TU.displayId)\n", - "\n", - " # l1 backbone zselection\n", - " backbone_fusion_sites = LVL2_FUSION_SITE_ORDER[i]\n", - " backbone = next(\n", - " plasmid\n", - " for plasmid in self_buildcompiler.indexed_backbones\n", - " if plasmid.fusion_sites == backbone_fusion_sites\n", - " and plasmid.antibiotic_resistance == KAN\n", - " )\n", - "\n", - " print(backbone)\n", - "\n", - " # TODO insert check here to see if the TU exists already (#43). should not be too expensive, as long as we search only indexed_plasmids where AR=KAN\n", - " composite_plasmids, final_doc = self_buildcompiler.assembly_lvl1(\n", - " TU, backbone=backbone, product_name=f\"{TU.displayId}_plas\"\n", - " )\n", - "\n", - " simplified_representation, new_defs = self_buildcompiler._encapsulate_TU(\n", - " composite_plasmids[0]\n", - " )\n", - " final_doc.add_list(new_defs)\n", - " lvl1_plasmids.append(simplified_representation)\n", - " print(simplified_representation)\n", - "\n", - " final_doc.write(\"encap.xml\")\n", - "\n", - " # get l2 backbone\n", - " plasmid_dict = {}\n", - " for p in lvl1_plasmids:\n", - " key = p.plasmid_definition.displayId\n", - " plasmid_dict.setdefault(key, []).append(p)\n", - "\n", - " backbone, _ = self_buildcompiler._get_backbone(\n", - " plasmid_dict, antibiotic_resistance=AMP\n", - " )\n", - "\n", - " print(backbone)\n", - "\n", - " # BbsI for l2\n", - " if self_buildcompiler.BbsI_impl is None:\n", - " self_buildcompiler._create_RE_implementation(\"BbsI\")\n", - " warnings.warn(\n", - " \"BbsI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.\",\n", - " RuntimeWarning,\n", - " )\n", - "\n", - " # TODO see about making these common enzymes (BsaI, BbSI, T4) global or class variables, so they only need to be searched for once\n", - " if self_buildcompiler.T4_ligase_impl is None:\n", - " self_buildcompiler._create_ligase_implementation()\n", - " warnings.warn(\n", - " \"No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.\",\n", - " RuntimeWarning,\n", - " )\n", - "\n", - " assembly = Assembly(\n", - " lvl1_plasmids,\n", - " backbone,\n", - " self_buildcompiler.BbsI_impl,\n", - " self_buildcompiler.T4_ligase_impl,\n", - " self_buildcompiler.sbol_doc,\n", - " final_doc,\n", - " product_name,\n", - " )\n", - "\n", - " lvl2_plasmids, final_doc = assembly.run() # TODO upload product_doc?\n", - " self_buildcompiler.indexed_plasmids.extend(lvl2_plasmids)\n", - "\n", - " return lvl2_plasmids, final_doc" + "print(buildcompiler.BbsI_impl, buildcompiler.BsaI_impl, buildcompiler.T4_ligase_impl)" ] }, { "cell_type": "code", - "execution_count": 5, - "id": "3e2272ff", + "execution_count": null, + "id": "6ec9e2fe", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Gen\n", - "Plasmid:\n", - " Name: DVK_AE_A_E\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_AE/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/DVK_AE_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['A', 'E']\n", - " Antibiotic Resistance: Kanamycin\n", - "\n", "matched pJ23100_AB_A_B with DVK_AE_A_E on fusion site A!\n", - "matched pB0032_BC_B_C with pJ23100_AB_A_B on fusion site B!\n", - "matched pE0040_CD_C_D with pB0032_BC_B_C on fusion site C!\n", - "matched final component pB0015_DE_D_E with pE0040_CD_C_D and DVK_AE_A_E on fusion sites (D, E)!\n", - "Plasmid:\n", - " Name: Gen_plas_1_simple_A_E\n", - " Plasmid Definition: https://SBOL2Build.org/Gen_plas_1_simple/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://SBOL2Build.org/Gen_plas_1_impl/1']\n", - " Strain Implementations: [None]\n", - " Fusion Sites: ['A', 'E']\n", - " Antibiotic Resistance: Kanamycin\n", - "\n", - "Gen1\n", - "Plasmid:\n", - " Name: DVK_EF_E_F\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_EF/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/DVK_EF_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['E', 'F']\n", - " Antibiotic Resistance: Kanamycin\n", - "\n" + "matched pB0034_BC_B_C with pJ23100_AB_A_B on fusion site B!\n", + "matched pE0030_CD_C_D with pB0034_BC_B_C on fusion site C!\n", + "matched final component pB0015_DE_D_E with pE0030_CD_C_D and DVK_AE_A_E on fusion sites (D, E)!\n", + "Success with backbone: DVK_AE_A_E and plasmids: ['pJ23100_AB_A_B', 'pB0034_BC_B_C', 'pE0030_CD_C_D', 'pB0015_DE_D_E']\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ - "/Users/ryan/GitRepo/SBOL2Build/src/buildcompiler/buildcompiler.py:320: RuntimeWarning: BsaI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.\n", + "/Users/ryan/GitRepo/SBOL2Build/src/buildcompiler/buildcompiler.py:321: RuntimeWarning: BsaI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.\n", " warnings.warn(\n" ] }, @@ -231,58 +128,73 @@ "name": "stdout", "output_type": "stream", "text": [ - "matched pJ23116_EB_E_B with DVK_EF_E_F on fusion site E!\n", - "matched pB0033_BC_B_C with pJ23116_EB_E_B on fusion site B!\n", - "matched pE1010_CD_C_D with pB0033_BC_B_C on fusion site C!\n", - "matched final component pB0015_DF_D_F with pE1010_CD_C_D and DVK_EF_E_F on fusion sites (D, F)!\n", - "Plasmid:\n", - " Name: Gen1_plas_1_simple_E_F\n", - " Plasmid Definition: https://SBOL2Build.org/Gen1_plas_1_simple/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://SBOL2Build.org/Gen1_plas_1_impl/1']\n", - " Strain Implementations: [None]\n", - " Fusion Sites: ['E', 'F']\n", - " Antibiotic Resistance: Kanamycin\n", - "\n", - "matched Gen_plas_1_simple_A_E with DVA_AF_A_F on fusion site A!\n", - "matched final component Gen1_plas_1_simple_E_F with Gen_plas_1_simple_A_E and DVA_AF_A_F on fusion sites (E, F)!\n", - "Success with backbone: DVA_AF_A_F and plasmids: ['Gen_plas_1_simple_A_E', 'Gen1_plas_1_simple_E_F']\n", - "Plasmid:\n", - " Name: DVA_AF_A_F\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_AF/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/DVA_AF_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['A', 'F']\n", - " Antibiotic Resistance: Ampicillin\n", - "\n" + "matched pJ23116_AB_A_B with DVK_AE_A_E on fusion site A!\n", + "matched pB0034_BC_B_C with pJ23116_AB_A_B on fusion site B!\n", + "matched pE0030_CD_C_D with pB0034_BC_B_C on fusion site C!\n", + "matched final component pB0015_DE_D_E with pE0030_CD_C_D and DVK_AE_A_E on fusion sites (D, E)!\n" ] }, { - "name": "stderr", - "output_type": "stream", - "text": [ - "/var/folders/ss/w4r72t4j057bp2m46gq_kjwr0000gn/T/ipykernel_78396/3312876232.py:92: RuntimeWarning: BbsI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.\n", - " warnings.warn(\n" - ] - }, + "data": { + "text/plain": [ + "({'https://sbolcanvas.org/i6TQvNp91/1': [Plasmid:\n", + " Name: i6TQvNp91_composite_1_A_E\n", + " Plasmid Definition: https://SBOL2Build.org/i6TQvNp91_composite_1/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://SBOL2Build.org/i6TQvNp91_composite_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['A', 'E']\n", + " Antibiotic Resistance: Kanamycin],\n", + " 'https://sbolcanvas.org/i0mwvNcgH/1': [Plasmid:\n", + " Name: i0mwvNcgH_composite_1_A_E\n", + " Plasmid Definition: https://SBOL2Build.org/i0mwvNcgH_composite_1/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://SBOL2Build.org/i0mwvNcgH_composite_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['A', 'E']\n", + " Antibiotic Resistance: Kanamycin]},\n", + " )" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "product_doc = sbol2.Document()\n", + "\n", + "buildcompiler.assembly_lvl1(design_defs, product_doc)" + ] + }, + { + "cell_type": "markdown", + "id": "00834b0c", + "metadata": {}, + "source": [ + "## LVL 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e2272ff", + "metadata": {}, + "outputs": [ { - "ename": "ValueError", - "evalue": "Not supported number of products. Found: 0", + "ename": "NameError", + "evalue": "name 'assembly_lvl2' is not defined", "output_type": "error", "traceback": [ "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mValueError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[5]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m composite_plasmids, final_doc = \u001b[43massembly_lvl2\u001b[49m\u001b[43m(\u001b[49m\u001b[43mbuildcompiler\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mdesign_doc\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[4]\u001b[39m\u001b[32m, line 115\u001b[39m, in \u001b[36massembly_lvl2\u001b[39m\u001b[34m(self_buildcompiler, abstract_design_doc, backbone, product_name)\u001b[39m\n\u001b[32m 100\u001b[39m warnings.warn(\n\u001b[32m 101\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mNo appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 102\u001b[39m \u001b[38;5;167;01mRuntimeWarning\u001b[39;00m,\n\u001b[32m 103\u001b[39m )\n\u001b[32m 105\u001b[39m assembly = Assembly(\n\u001b[32m 106\u001b[39m lvl1_plasmids,\n\u001b[32m 107\u001b[39m backbone,\n\u001b[32m (...)\u001b[39m\u001b[32m 112\u001b[39m product_name,\n\u001b[32m 113\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m115\u001b[39m lvl2_plasmids, final_doc = \u001b[43massembly\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# TODO upload product_doc?\u001b[39;00m\n\u001b[32m 116\u001b[39m self_buildcompiler.indexed_plasmids.extend(lvl2_plasmids)\n\u001b[32m 118\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m lvl2_plasmids, final_doc\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/GitRepo/SBOL2Build/src/buildcompiler/sbol2build.py:83\u001b[39m, in \u001b[36mAssembly.run\u001b[39m\u001b[34m(self, include_extracted_parts)\u001b[39m\n\u001b[32m 80\u001b[39m \u001b[38;5;28mself\u001b[39m.extracted_parts.append(extracts_tuple_list[\u001b[32m0\u001b[39m][\u001b[32m0\u001b[39m])\n\u001b[32m 82\u001b[39m backbone_impl = \u001b[38;5;28mself\u001b[39m.backbone.plasmid_implementations[\u001b[32m0\u001b[39m]\n\u001b[32m---> \u001b[39m\u001b[32m83\u001b[39m extracts_tuple_list, _ = \u001b[43mbackbone_digestion\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 84\u001b[39m \u001b[43m \u001b[49m\u001b[43mbackbone_impl\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 85\u001b[39m \u001b[43m \u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mrestriction_enzyme\u001b[49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 86\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43massembly_activity\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 87\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43msource_document\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 88\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 90\u001b[39m append_extracts_to_doc(extracts_tuple_list, \u001b[38;5;28mself\u001b[39m.source_document)\n\u001b[32m 91\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m include_extracted_parts:\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/GitRepo/SBOL2Build/src/buildcompiler/sbol2build.py:626\u001b[39m, in \u001b[36mbackbone_digestion\u001b[39m\u001b[34m(reactant, restriction_enzymes, assembly_activity, document)\u001b[39m\n\u001b[32m 623\u001b[39m digested_reactant = ds_reactant.cut(restriction_enzymes_pydna)\n\u001b[32m 625\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(digested_reactant) < \u001b[32m2\u001b[39m \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(digested_reactant) > \u001b[32m3\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m626\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[32m 627\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mNot supported number of products. Found: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlen\u001b[39m(digested_reactant)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 628\u001b[39m )\n\u001b[32m 629\u001b[39m \u001b[38;5;66;03m# TODO select them based on content rather than size.\u001b[39;00m\n\u001b[32m 630\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m circular \u001b[38;5;129;01mand\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(digested_reactant) == \u001b[32m2\u001b[39m:\n", - "\u001b[31mValueError\u001b[39m: Not supported number of products. Found: 0" + "\u001b[31mNameError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[5]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m composite_plasmids, final_doc = \u001b[43massembly_lvl2\u001b[49m(buildcompiler, design_doc)\n", + "\u001b[31mNameError\u001b[39m: name 'assembly_lvl2' is not defined" ] } ], "source": [ - "composite_plasmids, final_doc = assembly_lvl2(buildcompiler, design_doc)" + "composite_plasmids, final_doc = buildcompiler.assembly_lvl2(None)" ] }, { diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index 7b2323e..b8e98e0 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -118,13 +118,13 @@ def _index_collections(self, collections: List[str]): elif sbol2.BIOPAX_PROTEIN in built_object.types: if RESTRICTION_ENZYME in built_object.roles: if ( - built_object.identity - == "http://rebase.neb.com/rebase/enz/BsaI.html" + "http://rebase.neb.com/rebase/enz/BsaI.html" + in built_object.wasDerivedFrom ): self.BsaI_impl = implementation elif ( - built_object.identity - == "http://rebase.neb.com/rebase/enz/BbsI.html" + "http://rebase.neb.com/rebase/enz/BbsI.html" + in built_object.wasDerivedFrom ): self.BbsI_impl = implementation elif LIGASE in built_object.roles: @@ -289,7 +289,7 @@ def assembly_lvl1( self, abstract_designs: List[sbol2.ComponentDefinition], final_doc: sbol2.Document = sbol2.Document(), - product_name: str = None, + product_name: str = "composite", backbone: Plasmid = None, ) -> Tuple[Dict, sbol2.Document]: """Assemble level-1 plasmids for each gene/transcriptional unit. @@ -337,16 +337,16 @@ def assembly_lvl1( self.T4_ligase_impl, self.sbol_doc, final_doc, - product_name, + f"{abstract_design.displayId}_{product_name}", ) - composite_plasmids, product_doc = assembly.run() # TODO upload product_doc? + composite_plasmids, final_doc = assembly.run() # TODO upload product_doc? self.indexed_plasmids.extend( composite_plasmids ) # see about using a wrapper function to do this, where it checks if the design already exists (like in index_collections). this way we avoid duplicate issues that might come with loading the abstract design definitions into the self.sbol_doc ahead of time assembly_dict[abstract_design.identity] = composite_plasmids - return assembly_dict, product_doc + return assembly_dict, final_doc # TODO: Create a SBOL representation of the assembly process, updating the SBOL Document. # Using he selected parts create the representation, you need Plasmids, BsaI and T4 Ligase. From a0f395c65c1baec617123ee1ea63364b063c0f37 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Wed, 29 Apr 2026 14:50:42 -0600 Subject: [PATCH 33/93] better doc search in get_or_pull --- src/buildcompiler/abstract_translator.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/buildcompiler/abstract_translator.py b/src/buildcompiler/abstract_translator.py index c4fc666..d82f7f4 100644 --- a/src/buildcompiler/abstract_translator.py +++ b/src/buildcompiler/abstract_translator.py @@ -140,9 +140,17 @@ def get_or_pull(doc, sbh, uri): Get an SBOL object from a Document. If missing, pull it from SynBioHub and retry. """ - if uri not in doc: + try: + return doc.get(uri) + + except Exception as e: + # Treat lookup failure as "not present" sbh.pull(uri, doc) - return doc.get(uri) + + try: + return doc.get(uri) + except Exception: + raise e def extract_combinatorial_design_parts( From fb76b9c4c1b48692c9359767ebc2206b385e4db5 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Wed, 29 Apr 2026 14:55:07 -0600 Subject: [PATCH 34/93] added combinatorial design support to lvl1 --- src/buildcompiler/buildcompiler.py | 177 +++++++++++++++++++++++------ 1 file changed, 141 insertions(+), 36 deletions(-) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index b8e98e0..c8b13b7 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -9,6 +9,7 @@ dna_componentdefinition_with_sequence, rebase_restriction_enzyme, ) +from sbol2build.abstract_translator import enumerate_design_variants from .abstract_translator import ( get_or_pull, get_compatible_plasmids, @@ -49,7 +50,7 @@ def __init__( collections: List[str], sbh_registry: str, auth_token: str, - sbol_doc: sbol2.Document, + sbol_doc: sbol2.Document = None, ): self.sbh = sbol2.PartShop(sbh_registry) self.sbh.key = auth_token @@ -287,7 +288,8 @@ def _remove_internal_bsai_sites(sequence: str) -> tuple[str, int]: def assembly_lvl1( self, - abstract_designs: List[sbol2.ComponentDefinition], + abstract_designs: List[sbol2.ComponentDefinition] + | sbol2.CombinatorialDerivation, final_doc: sbol2.Document = sbol2.Document(), product_name: str = "composite", backbone: Plasmid = None, @@ -303,48 +305,109 @@ def assembly_lvl1( """ assembly_dict = {} + if type(abstract_designs) is sbol2.CombinatorialDerivation: + abstract_design_def = self.sbol_doc.getComponentDefinition( + abstract_designs.masterTemplate + ) - for abstract_design in abstract_designs: - plasmid_dict = self._get_input_plasmids( - design=abstract_design, antibiotic_resistance=AMP + combinatorial_part_dict = self.extract_combinatorial_design_parts( + abstract_design_def, abstract_designs ) - if not backbone: - backbone, compatible_plasmids = self._get_backbone( - plasmid_dict, antibiotic_resistance=KAN - ) - else: - compatible_plasmids = get_compatible_plasmids(plasmid_dict, backbone) - - if self.BsaI_impl is None: - self._create_RE_implementation("BsaI") - warnings.warn( - "BsaI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.", - RuntimeWarning, + enumerated_part_lists = enumerate_design_variants(combinatorial_part_dict) + + for i, list in enumerate(enumerated_part_lists): + plasmid_dict = self._construct_plasmid_dict(list, "Ampicillin") + + if not backbone: + backbone, compatible_plasmids = self._get_backbone( + plasmid_dict, antibiotic_resistance=KAN + ) + else: + compatible_plasmids = get_compatible_plasmids( + plasmid_dict, backbone + ) + + if self.BsaI_impl is None: + self._create_RE_implementation("BsaI") + warnings.warn( + "BsaI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.", + RuntimeWarning, + ) + + if self.T4_ligase_impl is None: + self._create_ligase_implementation() + warnings.warn( + "No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.", + RuntimeWarning, + ) + + assembly = Assembly( + compatible_plasmids, + backbone, + self.BsaI_impl, + self.T4_ligase_impl, + self.sbol_doc, + final_doc, + f"{abstract_design_def.displayId}_{product_name}_comb{i}", ) + composite_plasmids, final_doc = ( + assembly.run() + ) # TODO upload product_doc? + + self.indexed_plasmids.extend( + composite_plasmids + ) # see about using a wrapper function to do this, where it checks if the design already exists (like in index_collections). this way we avoid duplicate issues that might come with loading the abstract design definitions into the self.sbol_doc ahead of time - if self.T4_ligase_impl is None: - self._create_ligase_implementation() - warnings.warn( - "No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.", - RuntimeWarning, + assembly_dict.setdefault(abstract_design_def.identity, []).extend( + composite_plasmids + ) + else: + for abstract_design in abstract_designs: + plasmid_dict = self._get_input_plasmids( + design=abstract_designs, antibiotic_resistance=AMP ) - assembly = Assembly( - compatible_plasmids, - backbone, - self.BsaI_impl, - self.T4_ligase_impl, - self.sbol_doc, - final_doc, - f"{abstract_design.displayId}_{product_name}", - ) - composite_plasmids, final_doc = assembly.run() # TODO upload product_doc? + if not backbone: + backbone, compatible_plasmids = self._get_backbone( + plasmid_dict, antibiotic_resistance=KAN + ) + else: + compatible_plasmids = get_compatible_plasmids( + plasmid_dict, backbone + ) + + if self.BsaI_impl is None: + self._create_RE_implementation("BsaI") + warnings.warn( + "BsaI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.", + RuntimeWarning, + ) + + if self.T4_ligase_impl is None: + self._create_ligase_implementation() + warnings.warn( + "No appropriate ligase found in provided collection(s). Domestication of T4 Ligase via purchase will be added to protocol.", + RuntimeWarning, + ) + + assembly = Assembly( + compatible_plasmids, + backbone, + self.BsaI_impl, + self.T4_ligase_impl, + self.sbol_doc, + final_doc, + f"{abstract_design.displayId}_{product_name}", + ) + composite_plasmids, final_doc = ( + assembly.run() + ) # TODO upload product_doc? - self.indexed_plasmids.extend( - composite_plasmids - ) # see about using a wrapper function to do this, where it checks if the design already exists (like in index_collections). this way we avoid duplicate issues that might come with loading the abstract design definitions into the self.sbol_doc ahead of time - assembly_dict[abstract_design.identity] = composite_plasmids + self.indexed_plasmids.extend( + composite_plasmids + ) # see about using a wrapper function to do this, where it checks if the design already exists (like in index_collections). this way we avoid duplicate issues that might come with loading the abstract design definitions into the self.sbol_doc ahead of time + assembly_dict[abstract_design.identity] = composite_plasmids return assembly_dict, final_doc @@ -589,6 +652,48 @@ def _extract_design_parts( for component in component_list ] + def extract_combinatorial_design_parts( + self, + design: sbol2.ComponentDefinition, + derivation: sbol2.CombinatorialDerivation, + ) -> Dict[str, List[sbol2.ComponentDefinition]]: + """ + Extracts and returns a mapping of component definitions from a combinatorial design, in order. + Variants of combinatinatorial components are entered in a list corresponding to the URI of the component in the abstract design. + + Args: + design: + The top-level :class:`sbol2.ComponentDefinition` representing the + abstract design template whose components should be extracted in + sequential order. + + derivation: + The :class:`sbol2.CombinatorialDerivation` associated with ``design`` + that defines variable components and their allowed variants. + + Returns: + Dict[str, List[sbol2.ComponentDefinition]]: + A dictionary mapping component identities to lists + of variable component definitions. + + - Sequential design components map to lists containing a single definition. + - Combinatorial variable components map to lists of variant definitions. + """ + component_list = [c for c in design.getInSequentialOrder()] + component_dict = { + component.identity: [ + get_or_pull(self.sbol_doc, self.sbh, component.definition) + ] + for component in component_list + } + + for component in derivation.variableComponents: + component_dict[component.variable] = [ + self.sbol_doc.getComponentDefinition(var) for var in component.variants + ] + + return component_dict + def _get_abstract_design(self) -> sbol2.ComponentDefinition: for definition in self.sbol_doc.componentDefinitions: if ( From 44a0ae398ba97d8cf7bbd8194f2d945120487bcc Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:13:09 -0600 Subject: [PATCH 35/93] Add SBOL2 Transformation class and wire BuildCompiler transformation --- src/buildcompiler/buildcompiler.py | 90 ++++++---------------- src/buildcompiler/sbol2build.py | 119 +++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+), 65 deletions(-) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index 21fbea9..d271ad0 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -9,7 +9,11 @@ from typing import Any, Dict, List from buildcompiler.plasmid import Plasmid -from buildcompiler.sbol2build import Assembly, dna_componentdefinition_with_sequence +from buildcompiler.sbol2build import ( + Assembly, + Transformation as SBOL2Transformation, + dna_componentdefinition_with_sequence, +) from .abstract_translator import ( enumerate_design_variants, extract_combinatorial_design_parts, @@ -414,76 +418,32 @@ def transformation( chassis_module, chassis_impl = self._get_or_create_chassis( transformation_doc, chassis_name ) + normalized_plasmids = [] + for product in normalized_products: + indexed = self._get_indexed_plasmid(self.indexed_plasmids, product["plasmid"]) + if indexed is None: + indexed = type( + "TransformationPlasmid", + (), + { + "plasmid_definition": product["plasmid"], + "plasmid_implementations": [], + "name": product["plasmid"].displayId, + }, + )() + normalized_plasmids.append(indexed) + + sbol_outputs = SBOL2Transformation( + plasmids=normalized_plasmids, + chassis_name=chassis_name, + source_document=transformation_doc, + ).chemical_transformation() - sbol_outputs = [] robot_steps = [] logs = [] for index, product in enumerate(normalized_products, start=1): plasmid = product["plasmid"] - plasmid_impl = self._get_or_create_plasmid_implementation( - transformation_doc, plasmid - ) - transform_id = f"transform_{plasmid.displayId}_{index}" - - transformation_activity = sbol2.Activity(transform_id) - transformation_activity.name = f"Transform {chassis_name} with {plasmid.displayId}" - transformation_activity.types = "http://sbols.org/v2#build" - - chassis_usage = sbol2.Usage( - uri=f"{transform_id}_chassis_usage", - entity=chassis_impl.identity, - role="http://sbols.org/v2#build", - ) - plasmid_usage = sbol2.Usage( - uri=f"{transform_id}_plasmid_usage", - entity=plasmid_impl.identity, - role="http://sbols.org/v2#build", - ) - transformation_activity.usages = [chassis_usage, plasmid_usage] - - transformed_strain = sbol2.ModuleDefinition( - f"{chassis_name}_with_{plasmid.displayId}" - ) - transformed_strain.roles = [ORGANISM_STRAIN] - transformed_strain.name = f"{chassis_name} transformed with {plasmid.displayId}" - - chassis_module_ref = sbol2.Module( - uri=f"{transformed_strain.displayId}_chassis_module" - ) - chassis_module_ref.definition = chassis_module.identity - plasmid_fc = sbol2.FunctionalComponent( - uri=f"{transformed_strain.displayId}_plasmid_fc" - ) - plasmid_fc.definition = plasmid.identity - - transformed_strain.modules = [chassis_module_ref] - transformed_strain.functionalComponents = [plasmid_fc] - - transformed_impl = sbol2.Implementation( - f"{transformed_strain.displayId}_impl" - ) - transformed_impl.built = transformed_strain.identity - transformed_impl.wasGeneratedBy = transformation_activity.identity - - for obj in ( - transformation_activity, - chassis_usage, - plasmid_usage, - transformed_strain, - chassis_module_ref, - plasmid_fc, - transformed_impl, - ): - self._add_if_absent(transformation_doc, obj) - - sbol_outputs.append( - { - "transformation_activity": transformation_activity.identity, - "transformed_strain_module": transformed_strain.identity, - "transformed_strain_implementation": transformed_impl.identity, - } - ) robot_steps.append( { "step": index, diff --git a/src/buildcompiler/sbol2build.py b/src/buildcompiler/sbol2build.py index 4298c18..92aa36d 100644 --- a/src/buildcompiler/sbol2build.py +++ b/src/buildcompiler/sbol2build.py @@ -142,6 +142,125 @@ def initialize_assembly_activity(self): return activity +class Transformation: + """Create SBOL transformation records from plasmid/chassis inputs. + + Mirrors the orchestration pattern used by :class:`Assembly`, but for + chemical transformation into a chassis strain. + """ + + def __init__( + self, + plasmids: List[Plasmid], + chassis_name: str, + source_document: sbol2.Document, + ): + self.plasmids = plasmids + self.chassis_name = chassis_name + self.source_document = source_document + + def _add_if_absent(self, obj): + if self.source_document.find(obj.identity) is None: + self.source_document.add(obj) + + def _get_or_create_chassis(self) -> tuple[sbol2.ModuleDefinition, sbol2.Implementation]: + chassis_module = self.source_document.find(self.chassis_name) or sbol2.ModuleDefinition( + self.chassis_name + ) + chassis_module.name = self.chassis_name + self._add_if_absent(chassis_module) + + chassis_impl_id = f"{self.chassis_name}_impl" + chassis_impl = self.source_document.find(chassis_impl_id) or sbol2.Implementation( + chassis_impl_id + ) + chassis_impl.built = chassis_module.identity + self._add_if_absent(chassis_impl) + return chassis_module, chassis_impl + + def chemical_transformation(self) -> list[dict]: + chassis_module, chassis_impl = self._get_or_create_chassis() + outputs = [] + + for index, plasmid in enumerate(self.plasmids, start=1): + plasmid_def = plasmid.plasmid_definition + plasmid_impl = ( + plasmid.plasmid_implementations[0] + if plasmid.plasmid_implementations + else None + ) + if plasmid_impl is None: + plasmid_impl_id = f"{plasmid_def.displayId}_impl" + plasmid_impl = self.source_document.find(plasmid_impl_id) or sbol2.Implementation( + plasmid_impl_id + ) + plasmid_impl.built = plasmid_def.identity + self._add_if_absent(plasmid_impl) + + transform_id = f"transform_{plasmid_def.displayId}_{index}" + transformation_activity = sbol2.Activity(transform_id) + transformation_activity.name = ( + f"Transform {self.chassis_name} with {plasmid_def.displayId}" + ) + transformation_activity.types = "http://sbols.org/v2#build" + + chassis_usage = sbol2.Usage( + uri=f"{transform_id}_chassis_usage", + entity=chassis_impl.identity, + role="http://sbols.org/v2#build", + ) + plasmid_usage = sbol2.Usage( + uri=f"{transform_id}_plasmid_usage", + entity=plasmid_impl.identity, + role="http://sbols.org/v2#build", + ) + transformation_activity.usages = [chassis_usage, plasmid_usage] + + transformed_strain = sbol2.ModuleDefinition( + f"{self.chassis_name}_with_{plasmid_def.displayId}" + ) + transformed_strain.name = ( + f"{self.chassis_name} transformed with {plasmid_def.displayId}" + ) + chassis_module_ref = sbol2.Module( + uri=f"{transformed_strain.displayId}_chassis_module" + ) + chassis_module_ref.definition = chassis_module.identity + plasmid_fc = sbol2.FunctionalComponent( + uri=f"{transformed_strain.displayId}_plasmid_fc" + ) + plasmid_fc.definition = plasmid_def.identity + transformed_strain.modules = [chassis_module_ref] + transformed_strain.functionalComponents = [plasmid_fc] + + transformed_impl = sbol2.Implementation( + f"{transformed_strain.displayId}_impl" + ) + transformed_impl.built = transformed_strain.identity + transformed_impl.wasGeneratedBy = transformation_activity.identity + + for obj in ( + transformation_activity, + chassis_usage, + plasmid_usage, + transformed_strain, + chassis_module_ref, + plasmid_fc, + transformed_impl, + ): + self._add_if_absent(obj) + + outputs.append( + { + "transformation_activity": transformation_activity.identity, + "transformed_strain_module": transformed_strain.identity, + "transformed_strain_implementation": transformed_impl.identity, + } + ) + + return outputs + + def rebase_restriction_enzyme(name: str, **kwargs) -> sbol2.ComponentDefinition: """Creates an ComponentDefinition Restriction Enzyme Component from rebase. From 2665860b8ed9d905cfbd6720a0e755f1612098cb Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:28:58 -0600 Subject: [PATCH 36/93] Refactor plating to metadata and layout artifact outputs --- src/buildcompiler/buildcompiler.py | 148 +++++++---------------------- tests/test_plating.py | 9 +- 2 files changed, 37 insertions(+), 120 deletions(-) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index d271ad0..c1e646a 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -5,6 +5,7 @@ import shutil import warnings import urllib.parse +import csv from pathlib import Path from typing import Any, Dict, List @@ -489,14 +490,19 @@ def plating( plating_doc: sbol2.Document | None = None, overwrite: bool = False, ) -> Dict[str, Any]: - """Generate a plated 96-well output and protocol artifacts.""" + """Generate plating layout artifacts and protocol metadata. + + This implementation is file/metadata oriented and does not create new + SBOL objects for plating. + """ if protocol_type not in {"manual", "automated"}: raise ValueError("protocol_type must be one of: 'manual', 'automated'.") - if plating_doc is None: - plating_doc = self.sbol_doc advanced_params = advanced_params or {} + doc_ref = plating_doc or self.sbol_doc - normalized = normalize_plating_input(transformation_results, doc=plating_doc) + normalized = normalize_plating_input( + transformation_results, doc=doc_ref + ) if len(normalized) > 96: raise ValueError("plating supports up to 96 transformed strains.") @@ -505,69 +511,14 @@ def plating( results_path.mkdir(parents=True, exist_ok=True) plate_id = plate_name or "solid_96_well_plate" - plate_impl = sbol2.Implementation(plate_id) - plate_md = plating_doc.find("solid_96_well_plate_md") or sbol2.ModuleDefinition( - "solid_96_well_plate_md" - ) - plate_md.name = "Solid 96-well plate" - self._add_if_absent(plating_doc, plate_md) - plate_impl.built = plate_md.identity - self._add_if_absent(plating_doc, plate_impl) - - # Optional SBOLInventory integration with fallback behavior. - try: - from sbol_inventory import ( # type: ignore - make_solid_96_well_plate, - make_plated_strain, - place_in_plate, - ) - - inventory_enabled = True - inventory_plate = make_solid_96_well_plate( - uri=plate_impl.identity, plate_md_uri=plate_md.identity - ) - except Exception: - inventory_enabled = False - inventory_plate = None - - activity_id = f"plating_{protocol_type}_{plate_id}" - plating_activity = sbol2.Activity(activity_id) - plating_activity.name = f"Plating activity for {plate_id}" - plating_activity.types = "http://sbols.org/v2#build" - self._add_if_absent(plating_doc, plating_activity) - - agent_id = ( - "manual_plating_agent" - if protocol_type == "manual" - else "opentrons_plating_agent" - ) - agent = plating_doc.find(agent_id) or sbol2.Agent(agent_id) - agent.name = "Manual plating agent" if protocol_type == "manual" else "Opentrons plating agent" - self._add_if_absent(plating_doc, agent) - - plan_id = f"{plate_id}_{protocol_type}_plating_plan" - plan = plating_doc.find(plan_id) or sbol2.Plan(plan_id) - plan.name = f"{protocol_type.title()} plating plan for {plate_id}" - self._add_if_absent(plating_doc, plan) - - association = sbol2.Association( - uri=f"{activity_id}_association", - agent=agent.identity, - role="http://sbols.org/v2#build", - ) - association.plan = plan.identity - plating_activity.associations = [association] - self._add_if_absent(plating_doc, association) - plate_rows = [] plate_map = {} bacterium_locations = {} - plated_impls = [] for idx, entry in enumerate(normalized): well = wells[idx] source_impl_uri = entry.get("source_impl_uri") - source_impl = plating_doc.find(source_impl_uri) if source_impl_uri else None + source_impl = doc_ref.find(source_impl_uri) if source_impl_uri else None strain_module_uri = entry.get("strain_module_uri") if strain_module_uri is None and source_impl is not None: strain_module_uri = getattr(source_impl, "built", None) @@ -577,65 +528,31 @@ def plating( slug = parsed.path.split("/")[-1] if parsed.path else display_source slug = slug.replace("#", "_").replace(":", "_") - plated_module_id = f"{slug}_plated_{well}_md" - plated_module = plating_doc.find(plated_module_id) or sbol2.ModuleDefinition( - plated_module_id - ) - plated_module.roles = [ORGANISM_STRAIN] - plated_module.name = f"Plated strain {slug} at {well}" - if strain_module_uri: - plated_module.wasDerivedFrom = strain_module_uri - self._add_if_absent(plating_doc, plated_module) - plated_impl_id = f"{slug}_plated_{well}_impl" - plated_impl = plating_doc.find(plated_impl_id) or sbol2.Implementation( - plated_impl_id - ) - plated_impl.built = plated_module.identity - plated_impl.wasGeneratedBy = plating_activity.identity - if source_impl_uri: - plated_impl.wasDerivedFrom = source_impl_uri - self._add_if_absent(plating_doc, plated_impl) - plated_impls.append(plated_impl.identity) - - usage = sbol2.Usage( - uri=f"{activity_id}_usage_{idx+1}", - entity=source_impl_uri or plated_module.identity, - role=PLATING_ACTIVITY_ROLE, - ) - self._add_if_absent(plating_doc, usage) - current_usages = list(plating_activity.usages) - current_usages.append(usage) - plating_activity.usages = current_usages - - if inventory_enabled: - try: - inventory_plated = make_plated_strain( - uri=plated_impl.identity, - strain_md_uri=strain_module_uri or plated_module.identity, - design_uri=source_impl_uri, - ) - place_in_plate(inventory_plate, inventory_plated, well) - except Exception: - inventory_enabled = False - - plate_map[well] = plated_impl.identity - display_name = plated_module.displayId + plate_map[well] = plated_impl_id + display_name = plated_impl_id bacterium_locations[well] = display_name plate_rows.append( { "well": well, "source_transformed_strain_implementation": source_impl_uri, "strain_module": strain_module_uri, - "plated_strain_implementation": plated_impl.identity, + "plated_strain_implementation": plated_impl_id, "strain_display_name": display_name, } ) + plate_layout_csv = results_path / "plate_layout_dataframe.csv" + with plate_layout_csv.open("w", newline="", encoding="utf-8") as handle: + writer = csv.DictWriter(handle, fieldnames=list(plate_rows[0].keys()) if plate_rows else ["well"]) + writer.writeheader() + for row in plate_rows: + writer.writerow(row) + plate_map_json_path = write_plate_map_json( results_path / "plate_map.json", { - "plate_implementation": plate_impl.identity, + "plate_implementation": plate_id, "protocol_type": protocol_type, "well_map": plate_rows, }, @@ -650,18 +567,23 @@ def plating( protocol_artifacts: Dict[str, Any] = { "plate_map_json": str(plate_map_json_path), "plate_map_csv": str(plate_map_csv_path), + "plate_layout_dataframe_csv": str(plate_layout_csv), "logs": logs, + "pudu": { + "runner_script": "https://github.com/MyersResearchGroup/PUDU/blob/main/scripts/run_sbol2plating_with_params.py", + "mode": protocol_type, + "advanced_params": advanced_params, + }, } if protocol_type == "manual": md_path = write_manual_plating_protocol( results_path / "manual_plating_protocol.md", - plate_id=plate_impl.displayId, + plate_id=plate_id, plate_rows=plate_rows, advanced_params=advanced_params, ) protocol_artifacts["manual_protocol_markdown"] = str(md_path) - plan.description = f"Manual protocol file: {md_path}" else: script_path = write_plating_protocol_script( results_path / "plating_ot2.py", @@ -669,7 +591,6 @@ def plating( advanced_params=advanced_params, ) protocol_artifacts["ot2_script"] = str(script_path) - plan.description = f"Automated protocol script: {script_path}" try: sim_zip = run_opentrons_script_to_zip( script_path, @@ -684,15 +605,12 @@ def plating( "stage": "plating", "protocol_type": protocol_type, "plate": { - "plate_implementation": plate_impl.identity, + "plate_implementation": plate_id, "plate_map": plate_map, }, - "sbol_artifacts": { - "plating_activity": plating_activity.identity, - "agent": agent.identity, - "plan": plan.identity, - "plate_implementation": plate_impl.identity, - "plated_strain_implementations": plated_impls, + "metadata": { + "plate_rows": plate_rows, + "layout_dataframe_columns": list(plate_rows[0].keys()) if plate_rows else [], }, "json_intermediate": { "plating_data": {"bacterium_locations": bacterium_locations}, diff --git a/tests/test_plating.py b/tests/test_plating.py index e0c1684..9989538 100644 --- a/tests/test_plating.py +++ b/tests/test_plating.py @@ -100,11 +100,10 @@ def test_plating_manual_outputs_and_provenance(self): ) self.assertTrue(Path(result["protocol_artifacts"]["plate_map_json"]).exists()) self.assertTrue(Path(result["protocol_artifacts"]["plate_map_csv"]).exists()) - - activity = self.doc.find(result["sbol_artifacts"]["plating_activity"]) - self.assertIsNotNone(activity) - self.assertEqual(len(activity.usages), 2) - self.assertTrue(len(activity.associations) >= 1) + self.assertTrue( + Path(result["protocol_artifacts"]["plate_layout_dataframe_csv"]).exists() + ) + self.assertIn("pudu", result["protocol_artifacts"]) @patch("buildcompiler.robotutils.subprocess.run") def test_plating_automated_script_and_sim_zip(self, mock_run): From b61cfb1b06aadda941eac85d786eb822fa0a5d13 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 08:25:55 -0600 Subject: [PATCH 37/93] Add files via upload --- ADR-001.md | 160 +++++++++++++ AGENT.md | 402 ++++++++++++++++++++++++++++++++ ARCHITECTURE.md | 599 ++++++++++++++++++++++++++++++++++++++++++++++++ PRODUCT.md | 189 +++++++++++---- README.md | 326 +++++++++++++++++--------- 5 files changed, 1520 insertions(+), 156 deletions(-) create mode 100644 ADR-001.md create mode 100644 AGENT.md create mode 100644 ARCHITECTURE.md diff --git a/ADR-001.md b/ADR-001.md new file mode 100644 index 0000000..076a6ff --- /dev/null +++ b/ADR-001.md @@ -0,0 +1,160 @@ +# ADR-001: Replace Prototype BuildCompiler Structure with Clean Architecture + +## Status + +Accepted. + +## Context + +The current BuildCompiler prototype proves important pieces of the desired workflow, especially level-1 assembly and SBOL digestion/ligation behavior. However, the current structure mixes too many responsibilities in a small number of files: + +- collection indexing +- SynBioHub pulling +- plasmid/backbone/reagent tracking +- domestication +- level-1 assembly +- placeholder level-2 assembly +- transformation +- plating +- PUDU JSON conversion +- manual protocol helpers +- Opentrons simulation helpers + +The new `full_build` vision requires a dependency-resolving compiler pipeline, partial success semantics, route optimization, structured blockers, approvals, graph/reporting, and optional protocol generation. Preserving old APIs and module boundaries would slow implementation and make Codex work harder to verify. + +## Decision + +Rewrite the internals around a clean architecture while keeping only the root package name `buildcompiler`. + +Do not preserve legacy APIs, compatibility wrappers, or old module boundaries. + +The target architecture separates: + +```text +api + public compiler object and options + +domain + stable contracts and normalized domain records + +planning + design classification, validation, combinatorial expansion + +execution + worklist and bounded full-build loop + +stages + biological build operations + +sbol + SBOL resolver and SBOL construction services + +inventory + indexed material lookup and compatibility selection + +adapters + PUDU and Opentrons handoff boundaries + +reporting + graph, summary, report, serialization +``` + +## Options considered + +### Option 1: Incrementally patch the existing `BuildCompiler` class + +Pros: + +- Smaller initial diff. +- May preserve existing notebooks temporarily. +- Easier to start quickly. + +Cons: + +- Reinforces the current god-class structure. +- Makes partial success and structured blockers awkward. +- Makes Codex tasks harder to scope. +- Encourages hidden compatibility behavior. +- Risks coupling PUDU/Opentrons side effects to compiler-only mode. + +### Option 2: Preserve public wrappers but move internals + +Pros: + +- Gives users a transition path. +- Reduces immediate breakage. + +Cons: + +- Adds compatibility traces the project does not need. +- Doubles API surface during a major refactor. +- Forces new contracts to fit old return shapes. +- Makes partial success harder to expose cleanly. + +### Option 3: Clean architecture rewrite with no compatibility traces + +Pros: + +- Best long-term module boundaries. +- Makes Codex tasks smaller and safer. +- Enables testable contracts before complex stages. +- Separates compiler-only mode from automation side effects. +- Treats expected build blockers as structured data. +- Supports route optimization, reporting, and approvals naturally. + +Cons: + +- Larger initial migration. +- Existing notebooks/imports will break. +- Requires disciplined milestone sequencing. +- Requires new documentation and tests early. + +## Rationale + +Option 3 is best now because the user explicitly prefers clean architecture over compatibility and wants the current repository treated as inspiration, not an API to preserve. + +The new `full_build` behavior is not a small extension. It is a compiler pipeline with: + +- planning +- inventory lookup +- route optimization +- staged execution +- retry loops +- structured missing inputs +- approval gates +- SBOL artifacts +- PUDU JSON adapters +- reporting + +Those concerns need explicit boundaries. + +## Consequences + +- The root package remains `buildcompiler`. +- Existing method signatures such as `compiler.assembly_lvl1(raw_component_definition)` do not need to survive. +- `BuildCompiler.__init__` becomes dependency-injected and lightweight. +- `BuildCompiler.from_synbiohub(...)` owns convenience loading/indexing. +- Stages use a uniform `run(request, context) -> StageResult` contract. +- Inventory records replace the old `Plasmid` class. +- PUDU/Opentrons are optional adapters, not core dependencies. +- Codex should implement in milestone order and keep tests passing at each step. + +## Risks and mitigations + +| Risk | Mitigation | +|---|---| +| Rewrite grows too large | Implement milestone-by-milestone with tests. | +| Working level-1 assembly regresses | Port current `Assembly` behavior mostly intact before deeper cleanup. | +| Users lose old APIs | Accepted tradeoff; update docs and examples for new API. | +| SBOL identity bugs | Use identity-based domain contracts and central `SbolResolver`. | +| Route optimizer is hard to debug | Use explicit score dataclasses and detailed report alternatives. | +| Automation dependencies destabilize core CI | Keep PUDU/Opentrons optional and out of default CI. | + +## Revisit triggers + +Revisit this decision if: + +- A downstream user requires stable old APIs for publication or production workflows. +- SynBioSuite integration depends on old imports. +- The clean rewrite cannot pass level-1 fixture tests after the assembly service port. +- A future release needs a compatibility bridge for migration. diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..bda13fb --- /dev/null +++ b/AGENT.md @@ -0,0 +1,402 @@ +# Agent Guide for BuildCompiler + +## Mission + +Codex should implement the BuildCompiler clean-architecture refactor in small, verifiable increments. The goal is not to preserve legacy APIs. The current repository is inspiration and evidence, especially for level-1 assembly and SBOL digestion/ligation behavior. + +## Role split + +### ChatGPT owns + +- Reducing ambiguity. +- Revising architecture and product decisions. +- Breaking work into milestones and tasks. +- Deciding between alternatives when requirements conflict. +- Reviewing whether Codex output matches the intended plan. +- Updating this guidance when product or architecture decisions change. + +### Codex owns + +- Creating and editing repository files. +- Implementing scoped tasks. +- Refactoring within approved boundaries. +- Adding tests. +- Running checks. +- Reporting assumptions, blockers, and deviations. + +When a decision changes architecture, product scope, biological behavior, or public API semantics, Codex should stop and ask ChatGPT/user for a decision before continuing. + +## Hard rules + +1. Prefer small, incremental changes. +2. Keep the project runnable after every task. +3. Write tests for non-trivial logic. +4. Update docs when architecture, behavior, or commands change. +5. Do not preserve legacy APIs unless explicitly requested. +6. Do not add compatibility wrappers. +7. Do not silently expand scope. +8. Do not hide assumptions. +9. Do not make PUDU or Opentrons required for core tests. +10. Do not run Opentrons simulation by default. +11. Do not silently mutate biological sequences. +12. Do not silently assume reagent purchase. +13. Keep domain contracts identity-based where possible. +14. Keep raw `sbol2` objects at inventory/SBOL/stage boundaries. +15. Use `SbolResolver` instead of scattered direct SBOL document lookup. +16. Use structured blockers, warnings, and approvals instead of raw exceptions for expected build issues. +17. Raise unexpected implementation errors by default. + +## Definition of a safe Codex task + +A task is safe for Codex when it has: + +- A clear target file or module. +- A clear expected behavior. +- A test strategy. +- No unresolved product or architecture tradeoff. +- No need to choose between incompatible biological semantics. + +Examples: + +- Add `StageStatus`, `BuildStatus`, and tests. +- Implement `BuildOptions` dataclasses. +- Create `SbolResolver` with `PullPolicy.NEVER` unit tests. +- Add `Inventory.add_generated_product()` and indexing tests. +- Port `Assembly` behind `AssemblyService` without changing internals. + +## Definition of a risky task + +A task is risky when it: + +- Changes public API semantics. +- Changes build-stage behavior. +- Changes approval or sequence-edit policy. +- Changes route-selection ranking. +- Changes material-state semantics. +- Touches PUDU/Opentrons execution side effects. +- Requires choosing how SBOL structures should be interpreted. +- Would take more than one focused PR. + +Risky tasks should be decomposed or escalated to ChatGPT/user. + +## Handoff protocol + +Every Codex task should begin with: + +```text +Task goal: +Files expected to change: +Behavior expected: +Tests expected: +Known constraints: +``` + +Every Codex completion should report: + +```text +Implemented: +Tests added/updated: +Commands run: +Assumptions made: +Blockers or follow-up tasks: +``` + +If tests cannot be run, Codex must say why and identify the smallest command that should be run later. + +## Implementation sequence + +Implement in milestone order. Do not jump directly to the full-build loop before contracts and tests exist. + +### Milestone 1: Domain contracts and options + +Create: + +```text +src/buildcompiler/domain/ +src/buildcompiler/api/options.py +``` + +Add contracts for: + +- `BuildStage` +- `StageStatus` +- `BuildStatus` +- `MaterialState` +- `BuildRequest` +- `StageResult` +- `FullBuildResult` +- `MissingBuildInput` +- `RequiredApproval` +- `BuildWarning` +- `IndexedPlasmid` +- `IndexedBackbone` +- `IndexedReagent` +- `BuildOptions` and focused option groups + +Tests: + +```text +tests/unit/domain/ +tests/unit/api/test_options.py +``` + +### Milestone 2: SBOL resolver and inventory + +Create: + +```text +src/buildcompiler/sbol/resolver.py +src/buildcompiler/inventory/ +``` + +Implement: + +- `PullPolicy` +- `SbolResolver` +- normalized inventory records +- eager indexes +- generated-product indexing +- reagent lookup +- backbone lookup + +Tests should use offline SBOL fixtures and `PullPolicy.NEVER`. + +### Milestone 3: Planner and classification + +Create: + +```text +src/buildcompiler/planning/classifier.py +src/buildcompiler/planning/combinatorial.py +src/buildcompiler/planning/full_build_planner.py +src/buildcompiler/planning/validation.py +``` + +Implement classification rules: + +```text +ModuleDefinition -> assembly_lvl2 +CombinatorialDerivation -> expanded assembly_lvl1 requests +ComponentDefinition >1 component -> assembly_lvl1 +ComponentDefinition <=1 component with supported role -> domestication +ComponentDefinition <=1 component unsupported -> unsupported +``` + +Implement combinatorial cap and invalid variant warnings. + +### Milestone 4: Compatibility selector and route models + +Create: + +```text +src/buildcompiler/inventory/selector.py +src/buildcompiler/inventory/compatibility.py +``` + +Implement route scoring dataclasses for level-1 and level-2. Keep scores explicit, not opaque numbers. + +### Milestone 5: SBOL assembly service and level-1 stage + +Port current working `Assembly` behavior into: + +```text +src/buildcompiler/sbol/assembly.py +``` + +Wrap it with: + +```text +src/buildcompiler/stages/assembly_lvl1.py +``` + +Do not deeply refactor SBOL internals until tests protect the new service interface. + +### Milestone 6: Domestication stage + +Create: + +```text +src/buildcompiler/sbol/domestication.py +src/buildcompiler/stages/domestication.py +``` + +Implement: + +- supported role validation +- missing backbone handling +- missing reagent handling +- sequence edit proposals +- approval-gated execution behavior +- domesticated plasmid output + +### Milestone 7: Level-2 stage + +Create: + +```text +src/buildcompiler/stages/assembly_lvl2.py +``` + +Implement: + +- `ModuleDefinition` engineered-region extraction +- optional `region_order` constraint +- exhaustive order search up to 4 regions +- missing level-1 promotion +- selected route and top rejected alternatives + +### Milestone 8: Full-build executor + +Create: + +```text +src/buildcompiler/execution/full_build_executor.py +src/buildcompiler/execution/worklist.py +src/buildcompiler/execution/stage_runner.py +src/buildcompiler/execution/context.py +``` + +Implement bounded retry loop with mocked/stubbed stages first, then wire real stages. + +### Milestone 9: Transformation and plating + +Create: + +```text +src/buildcompiler/stages/transformation.py +src/buildcompiler/stages/plating.py +src/buildcompiler/sbol/transformation.py +``` + +Implement SBOL transformation records, PUDU JSON intermediates, plate mapping, and deduplication. + +### Milestone 10: Adapters, reporting, and integration tests + +Create: + +```text +src/buildcompiler/adapters/pudu/ +src/buildcompiler/adapters/opentrons/ +src/buildcompiler/reporting/ +``` + +Implement: + +- assembly/transformation/plating JSON adapters +- optional manual/automated protocol file generation +- optional Opentrons simulation wrapper +- `BuildSummary` +- opt-in `BuildReport` +- reporting-only `BuildGraph` +- end-to-end happy-path fixture test + +## Testing and verification expectations + +Use pytest and Ruff for default CI. + +Core commands: + +```bash +uv run ruff check . +uv run ruff format --check . +uv run pytest +``` + +Fallback without `uv`: + +```bash +ruff check . +ruff format --check . +pytest +``` + +Automation-specific tests should be marked and optional: + +```bash +pytest -m automation +``` + +Suggested test layout: + +```text +tests/ + unit/ + domain/ + planning/ + inventory/ + execution/ + reporting/ + stages/ + test_assembly_lvl1.py + test_domestication.py + test_assembly_lvl2.py + test_transformation.py + test_plating.py + integration/ + test_full_build_happy_path.py + test_full_build_missing_lvl1_then_domestication.py + automation/ + test_pudu_json_adapters.py + test_opentrons_simulation_smoke.py + fixtures/ + sbol/ + expected_json/ +``` + +## Documentation update rules + +Update docs in the same PR when: + +- public API changes +- package layout changes +- option defaults change +- stage behavior changes +- status semantics change +- approval behavior changes +- route scoring changes +- test commands or CI commands change + +## ADR trigger conditions + +Create or update an ADR when: + +- There are multiple reasonable architecture options. +- A decision changes module boundaries. +- A decision changes biological interpretation. +- A decision changes public API. +- A decision adds or removes a stage. +- A decision changes default safety behavior. +- A decision adds persistent side effects. + +## Progress reporting style + +Codex should keep progress compact and evidence-based: + +```text +Done: +- Added domain status enums and tests. +- Added BuildOptions groups with defaults. + +Verified: +- uv run pytest tests/unit/domain tests/unit/api +- uv run ruff check src tests + +Assumptions: +- MaterialState.PLANNED ranks below ASSEMBLED and above missing material. + +Next: +- Implement SbolResolver with PullPolicy.NEVER tests. +``` + +## When to stop and ask + +Stop and ask ChatGPT/user before: + +- Changing approval semantics. +- Changing level-1 cardinality. +- Supporting new part roles. +- Changing level-2 route scoring. +- Making graph scheduling drive execution. +- Making PUDU/Opentrons required. +- Adding persistent approvals. +- Adding DNA extraction as a real stage. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..801e6ca --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,599 @@ +# BuildCompiler Architecture + +## System overview + +BuildCompiler is a Python compiler pipeline for synthetic biology build planning. It takes SBOL designs and biological inventory, then produces structured build plans, SBOL build artifacts, PUDU-compatible JSON intermediates, and optional protocol-generation artifacts. + +The architecture separates: + +```text +planning: classify and expand what should be built +execution: decide what to run and retry +stages: perform biological build operations +inventory: answer what material exists +sbol: construct and resolve SBOL objects +adapters: translate BuildCompiler outputs to external formats +reporting: explain what happened and what to do next +``` + +## Design principles + +1. **Compiler first, automation second.** Core planning and SBOL/JSON generation should run without PUDU or Opentrons. +2. **Contracts before implementation.** Domain dataclasses define stable boundaries between planner, executor, stages, inventory, and reporting. +3. **Partial success is normal.** Stages may produce some products while reporting missing inputs or approvals. +4. **Expected blockers are data.** Missing plasmids, backbones, reagents, approvals, and unsupported designs should be structured records. +5. **Unexpected bugs are errors.** Implementation errors should raise by default unless `continue_on_error=True`. +6. **Inventory and selection are separate.** Inventory answers what exists; selectors decide which valid route to use. +7. **SBOL identity is canonical.** Domain models should use SBOL identities as stable keys. Raw `sbol2` objects stay at inventory/SBOL/stage boundaries. +8. **The graph explains; it does not schedule.** The v1 build graph is reporting-only. + +## Package structure + +```text +src/buildcompiler/ + api/ Public user API and options + domain/ Pure contracts and normalized domain models + planning/ Classification, validation, combinatorial expansion + execution/ Worklist, context, bounded full-build loop + stages/ Domestication, lvl1, lvl2, transformation, plating + sbol/ SBOL construction, resolver, document helpers + inventory/ Indexes, SynBioHub loading, compatibility selection + adapters/ PUDU JSON/protocol and Opentrons simulation adapters + reporting/ Build graph, summary, detailed report, serialization + errors.py + logging.py +``` + +## Public API + +`BuildCompiler.__init__` is dependency-injected and lightweight: + +```python +compiler = BuildCompiler( + inventory=inventory, + sbol_document=doc, + planner=FullBuildPlanner(), + executor=FullBuildExecutor(), + adapters=AdapterRegistry(...), +) +``` + +Convenience construction handles SynBioHub loading and indexing: + +```python +compiler = BuildCompiler.from_synbiohub( + collections=collections, + sbh_registry=sbh_registry, + auth_token=auth_token, + sbol_doc=sbol_doc, +) +``` + +Primary workflow: + +```python +plan = compiler.plan(abstract_designs) +result = compiler.execute(plan) +``` + +Convenience wrapper: + +```python +result = compiler.full_build(abstract_designs, options=options) +``` + +## Core domain model + +### Status enums + +```python +class StageStatus(Enum): + SUCCESS = "success" + PARTIAL_SUCCESS = "partial_success" + BLOCKED = "blocked" + FAILED = "failed" + +class BuildStatus(Enum): + SUCCESS = "success" + PARTIAL_SUCCESS = "partial_success" + FAILED = "failed" +``` + +`BLOCKED` means expected inputs or approvals can unblock the stage later. `FAILED` means the request cannot proceed without changing the design, options, collections, or approval state. + +### BuildRequest + +```python +@dataclass +class BuildRequest: + id: str + stage: BuildStage + source_identity: str + source_display_id: str | None + source_kind: DesignKind + parent_group: str | None = None + variant_index: int | None = None + constraints: dict = field(default_factory=dict) +``` + +Core contracts should use SBOL identities and normalized metadata. Stages resolve identities to raw SBOL objects through `SbolResolver`. + +### StageResult + +```python +@dataclass +class StageResult: + id: str + stage: BuildStage + status: StageStatus + request_ids: list[str] + products: list[IndexedPlasmid] + missing_inputs: list[MissingBuildInput] + required_approvals: list[RequiredApproval] + warnings: list[BuildWarning] + sbol_document: sbol2.Document | None + json_intermediate: dict | list | None + protocol_artifacts: dict + logs: list[str] +``` + +### FullBuildResult + +```python +@dataclass +class FullBuildResult: + status: BuildStatus + plan: BuildPlan + build_document: sbol2.Document + stage_results: list[StageResult] + graph: BuildGraph + final_products: list[IndexedPlasmid] + missing_inputs: list[MissingBuildInput] + required_approvals: list[RequiredApproval] + warnings: list[BuildWarning] + summary: BuildSummary + report: BuildReport | None = None +``` + +### MissingBuildInput + +```python +@dataclass +class MissingBuildInput: + source_stage: BuildStage + source_design_identity: str + missing_identity: str + missing_display_id: str | None + missing_kind: Literal[ + "engineered_region", + "promoter", + "rbs", + "cds", + "terminator", + "backbone", + "restriction_enzyme", + "ligase", + ] + required_stage: BuildStage | Literal["fatal"] + reason: str + candidates_tried: list[str] = field(default_factory=list) +``` + +### RequiredApproval + +Approvals should be easy to grant by process name. + +```python +@dataclass +class RequiredApproval: + status: ApprovalStatus + process: str + reason: str + metadata: dict = field(default_factory=dict) + +@dataclass +class ApprovalOptions: + approved_processes: set[str] = field(default_factory=set) + approved_approval_ids: set[str] = field(default_factory=set) + scope: Literal["run", "persistent"] = "run" +``` + +Default approval scope is run-scoped. + +## Options model + +Use composed option groups: + +```python +@dataclass +class BuildOptions: + planning: PlanningOptions = field(default_factory=PlanningOptions) + execution: ExecutionOptions = field(default_factory=ExecutionOptions) + selection: SelectionOptions = field(default_factory=SelectionOptions) + protocol: ProtocolOptions = field(default_factory=ProtocolOptions) + reporting: ReportingOptions = field(default_factory=ReportingOptions) + approvals: ApprovalOptions = field(default_factory=ApprovalOptions) + reagents: ReagentOptions = field(default_factory=ReagentOptions) + domestication: DomesticationOptions = field(default_factory=DomesticationOptions) +``` + +Important defaults: + +```python +ExecutionOptions(max_iterations=5, continue_on_error=False) +ProtocolOptions(mode=ProtocolMode.NONE, simulate=False, results_dir=None) +ReportingOptions(include_detailed_report=False, include_rejected_routes=True, max_rejected_routes=3) +CombinatorialOptions(max_variants=256, allow_large_expansion=False) +Lvl2SearchOptions(max_exhaustive_region_count=4, allow_large_order_search=False) +ReagentOptions(allow_reagent_purchase=False, default_restriction_enzyme="BsaI", default_ligase="T4_DNA_ligase") +``` + +## BuildContext + +Every stage receives one explicit typed context: + +```python +@dataclass +class BuildContext: + sbol: SbolResolver + inventory: Inventory + build_document: sbol2.Document + options: BuildOptions + adapters: AdapterRegistry + graph: BuildGraph + logger: BuildLogger +``` + +Stage signature: + +```python +def run(request: BuildRequest, context: BuildContext) -> StageResult: + ... +``` + +## SBOL resolver + +Stages should not call `doc.get(...)`, `doc.find(...)`, or SynBioHub pull logic directly. Use `SbolResolver`. + +```python +class SbolResolver: + def get_component(self, identity: str) -> sbol2.ComponentDefinition: ... + def get_module(self, identity: str) -> sbol2.ModuleDefinition: ... + def get_combinatorial_derivation(self, identity: str) -> sbol2.CombinatorialDerivation: ... + def get_implementation(self, identity: str) -> sbol2.Implementation: ... + def maybe_pull(self, identity: str) -> object: ... +``` + +Pull policy: + +```python +class PullPolicy(Enum): + NEVER = "never" + MISSING_ONLY = "missing_only" + ALWAYS_REFRESH = "always_refresh" +``` + +Default to `MISSING_ONLY`; use `NEVER` for deterministic offline tests. + +## Inventory architecture + +Inventory owns normalized records and eager indexes. + +### IndexedPlasmid + +```python +@dataclass +class IndexedPlasmid: + identity: str + display_id: str + definition: sbol2.ComponentDefinition + implementations: list[sbol2.Implementation] + strain_definitions: list[sbol2.ModuleDefinition] + strain_implementations: list[sbol2.Implementation] + insert_identities: set[str] + fusion_sites: tuple[str, ...] + antibiotic_resistance: str | None + source: Literal["collection", "generated"] + generating_stage: BuildStage | None + material_state: MaterialState + provenance: dict +``` + +### IndexedBackbone and IndexedReagent + +```python +@dataclass +class IndexedBackbone: + identity: str + display_id: str + definition: sbol2.ComponentDefinition + implementations: list[sbol2.Implementation] + fusion_sites: tuple[str, ...] + antibiotic_resistance: str | None + material_state: MaterialState + source: Literal["collection", "generated"] + provenance: dict + +@dataclass +class IndexedReagent: + identity: str + display_id: str + name: str + kind: Literal["restriction_enzyme", "ligase", "other"] + definition: sbol2.ComponentDefinition + implementations: list[sbol2.Implementation] + source: Literal["collection", "generated", "assumed_purchase"] + provenance: dict +``` + +### Inventory facade + +```python +class Inventory: + plasmids_by_identity: dict[str, IndexedPlasmid] + plasmids_by_insert_identity: dict[str, list[IndexedPlasmid]] + plasmids_by_fusion_sites: dict[tuple[str, ...], list[IndexedPlasmid]] + plasmids_by_antibiotic: dict[str, list[IndexedPlasmid]] + backbones_by_fusion_sites_and_antibiotic: dict[tuple[tuple[str, ...], str], list[IndexedBackbone]] + + def find_single_part_plasmids(self, part_identity: str, *, antibiotic: str | None = None) -> list[IndexedPlasmid]: ... + def find_lvl1_region_plasmids(self, region_identity: str, *, min_material_state: MaterialState = MaterialState.PLANNED) -> list[IndexedPlasmid]: ... + def find_backbone(self, *, fusion_sites: tuple[str, ...] | None = None, antibiotic: str | None = None, stage: BuildStage | None = None) -> IndexedBackbone | None: ... + def find_restriction_enzyme(self, name: str) -> IndexedReagent | None: ... + def find_ligase(self, preferred: str | None = None) -> IndexedReagent | None: ... + def add_generated_product(self, product: IndexedPlasmid) -> None: ... +``` + +Inventory builds indexes eagerly and updates incrementally when generated products are added. + +## Compatibility selection + +`CompatibilitySelector` answers which valid route to use. Inventory should not own route-ranking policy. + +Default ranking: + +1. Respect request constraints. +2. Respect global selection options. +3. Prefer fewer new build products required. +4. Prefer existing collection material over generated/planned material. +5. Prefer higher material state. +6. Prefer exact antibiotic/fusion-site match. +7. Prefer simpler material routes. +8. Tie-break by stable SBOL identity. + +Selection overrides live in both `BuildOptions.selection` and `BuildRequest.constraints`. Request constraints win over global options. + +## Planning flows + +### Design classification + +`FullBuildPlanner` should classify designs into initial queues: + +```text +ModuleDefinition -> assembly_lvl2 +CombinatorialDerivation -> expand into assembly_lvl1 variant requests +ComponentDefinition with >1 component -> assembly_lvl1 +ComponentDefinition with <=1 component and supported role -> domestication +ComponentDefinition with <=1 component and unsupported role -> unsupported +``` + +### Combinatorial expansion + +- Expand valid concrete variants into level-1 requests. +- Validate each variant against level-1 cardinality. +- Skip invalid variants with structured warnings. +- Fail only if no valid variants remain. +- Default cap: `max_variants=256` unless `allow_large_expansion=True`. + +## Execution flow + +Use a bounded worklist/fixed-point loop: + +```python +for iteration in range(options.execution.max_iterations): + progress = False + + lvl2_results = run_buildable_lvl2_requests() + progress |= index_products(lvl2_results) + promote_missing_engineered_regions_to_lvl1(lvl2_results) + + lvl1_results = run_buildable_lvl1_requests() + progress |= index_products(lvl1_results) + promote_missing_parts_to_domestication(lvl1_results) + + domestication_results = run_buildable_domestication_requests() + progress |= index_products(domestication_results) + + chain_transformation_and_plating_for_new_products() + + if all_requests_resolved(): + break + if not progress: + break +``` + +Safeguards: + +- `max_iterations = 5` by default. +- Track `seen_products`, `seen_missing_inputs`, and `seen_requests`. +- Transform/plate each unique product only once. +- Raise unexpected implementation errors by default unless `continue_on_error=True`. + +## Stage responsibilities + +### Domestication stage + +Responsibilities: + +- Validate supported part role. +- Find required domestication backbone. +- Find BsaI and ligase reagents or return missing inputs. +- Propose internal BsaI sequence edits. +- Require approval in protocol/execution mode before using edited sequence. +- Create domesticated plasmid as primary product. +- Record insert and sequence-edit provenance as artifacts. + +Primary product: domesticated plasmid. + +### Assembly level 1 stage + +Responsibilities: + +- Resolve concrete level-1 design request. +- Extract exactly one promoter, one RBS, one CDS, one terminator. +- Use SBOL order if unambiguous. +- Warn if SBOL order differs from promoter/RBS/CDS/terminator. +- Use canonical order if SBOL order is ambiguous. +- Search candidate part plasmids and backbones. +- Select route minimizing new domestications. +- Return products or missing parts. +- Index products by plasmid identity and source engineered-region/design identity. + +### Assembly level 2 stage + +Responsibilities: + +- Resolve `ModuleDefinition` with engineered-region components. +- Use `region_order` constraint if provided. +- If order absent and region count <= 4, enumerate candidate orders. +- If no full route exists, choose route requiring fewest new level-1 plasmids. +- Return selected route plus top 3 rejected alternatives when detailed reporting is enabled. +- Return missing engineered regions as level-1 requests. + +### Transformation stage + +Responsibilities: + +- Consume generated plasmids. +- Create SBOL transformation records. +- Produce PUDU-compatible transformation JSON. +- Produce transformed strain records/products for plating. + +### Plating stage + +Responsibilities: + +- Consume transformed strain outputs. +- Produce deterministic well mapping. +- Return plate map dictionary as main result. +- Optionally write CSV mapping with strain implementation URIs and dilution metadata. +- Produce PUDU-compatible plating JSON. +- No SBOL output is required for plating v1. + +## SBOL assembly service + +Port the current working `Assembly` behavior mostly intact into `sbol/assembly.py`, behind a cleaner service interface. Refactor internals only after tests protect the new contract. + +```python +@dataclass +class AssemblyJob: + stage: BuildStage + product_identity: str + product_display_id: str + part_plasmids: list[IndexedPlasmid] + backbone: IndexedBackbone + restriction_enzyme: IndexedReagent + ligase: IndexedReagent + source_document: sbol2.Document + target_document: sbol2.Document + +@dataclass +class AssemblySbolResult: + products: list[IndexedPlasmid] + stage_document: sbol2.Document + activity_identity: str + logs: list[str] +``` + +## Adapter boundaries + +Adapters translate BuildCompiler outputs into external inputs. + +- Assembly PUDU JSON uses `Product`, `Backbone`, `PartsList`, and `Restriction Enzyme`. +- Transformation PUDU JSON uses `Strain`, `Chassis`, and `Plasmids`. +- Plating PUDU JSON uses `bacterium_locations` plus advanced parameters. + +Compiler-only mode always creates in-memory JSON intermediates. File writing, manual Markdown, OT-2 scripts, and simulation are optional. + +`simulate=True` is required for Opentrons simulation. Simulation is never default. + +## Reporting + +### BuildSummary + +Always generated. + +```python +@dataclass +class BuildSummary: + status: BuildStatus + final_product_count: int + missing_input_count: int + required_approval_count: int + warning_count: int + + def to_dict(self) -> dict: ... + def to_json(self) -> str: ... + def to_markdown(self) -> str: ... +``` + +### BuildReport + +Generated only when `include_detailed_report=True`. + +```python +@dataclass +class BuildReport: + status: BuildStatus + executive_summary: str + stage_sections: list[StageReportSection] + selected_routes: list[RouteReport] + rejected_alternatives: list[RouteReport] + missing_inputs: list[MissingInputReport] + required_approvals: list[ApprovalReport] + warnings: list[WarningReport] + next_actions: list[RecommendedAction] + dependency_chain: list[DependencyChainStep] + graph_summary: dict +``` + +`next_actions` tells the user what to do now. `dependency_chain` explains the path to final success. + +## Operational concerns + +### Configuration + +- Keep defaults safe and compiler-only. +- Put broad options in focused dataclasses. +- Make network pull behavior explicit through `PullPolicy`. +- Make approvals run-scoped by default. + +### Observability + +- Replace `print` calls with `BuildLogger`. +- Include per-stage logs in `StageResult`. +- Include candidate route scoring in detailed reports. + +### Security and safety + +- Do not silently mutate sequences. +- Do not silently assume reagent purchases. +- Do not silently expand very large combinatorial designs. +- Do not silently perform large level-2 order searches. +- Do not run robot simulations as side effects. + +### Testing + +Core tests should run offline where possible. SynBioHub network access should be isolated behind integration tests or explicitly marked tests. + +## Risks and mitigations + +| Risk | Mitigation | +|---|---| +| Clean rewrite becomes too large | Use milestone sequence and require passing tests at each layer. | +| SBOL object identity bugs | Keep canonical identity-based domain contracts and centralize lookup in `SbolResolver`. | +| Route optimizer complexity grows | Start with explicit scoring dataclasses and bounded search limits. | +| Current working level-1 assembly regresses | Port mostly intact first and protect with SBOL fixture tests before internal cleanup. | +| Optional PUDU/Opentrons dependencies destabilize CI | Keep automation tests optional/manual. | +| Sequence edits are unsafe | Require approval in execution/protocol mode and record provenance. | +| Reports become too verbose | Always generate lightweight summary; detailed report is opt-in. | diff --git a/PRODUCT.md b/PRODUCT.md index 2d5228d..ffed963 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -1,68 +1,169 @@ -# Product Overview +# Product Brief: BuildCompiler Full-Build Refactor -BuildCompiler is a tool for turning genetic designs into real biological builds. +## Problem ---- +Synthetic biology users can design constructs in SBOL, but turning those designs into executable build workflows still requires manual reasoning about available inventory, missing parts, assembly levels, transformation, plating, and automation handoff formats. -## 🎯 Vision +BuildCompiler should act like a compiler: inspect abstract designs, resolve dependencies against inventory, choose an implementable route, generate missing intermediates when possible, and report actionable blockers when not possible. -Make biological construction as easy as compiling code. +## Target users ---- +- Synthetic biology researchers designing constructs in SBOL/SBOLCanvas/SynBioSuite. +- Lab automation users generating manual or Opentrons-ready protocols through PUDU. +- Developers extending BuildCompiler's planning, inventory, SBOL, or protocol-generation behavior. +- AI coding agents such as Codex implementing scoped refactor tasks from an approved plan. -## 👥 Users +## Jobs to be done -- Synthetic biology researchers -- Biofoundries -- Automation engineers -- Computational biologists +1. As a researcher, I want to submit abstract SBOL designs and learn whether they can be built from available inventory. +2. As a build planner, I want missing level-1 engineered regions promoted into level-1 assembly requests. +3. As a build planner, I want missing promoter/RBS/CDS/terminator parts promoted into domestication requests. +4. As an automation user, I want SBOL and PUDU-compatible JSON outputs for assembly, transformation, and plating. +5. As a lab user, I want clear actions when a build is blocked by missing inventory, missing approvals, or unsupported designs. +6. As a developer, I want clear module boundaries so I can improve one stage without breaking the whole compiler. ---- +## Product goals -## 💡 What Makes It Different? +- Make `full_build` a deterministic, inspectable compiler pipeline. +- Support partial success and structured blockers. +- Minimize new build work by searching inventory before generating new requests. +- Keep compiler-only mode lightweight and testable. +- Make optional automation outputs explicit, not default side effects. +- Provide a clean architecture that Codex can implement in small, safe increments. -Unlike existing tools, BuildCompiler: +## Non-goals -- Starts from **abstract designs** -- Automatically resolves **available parts** -- Generates **complete lab workflows** -- Supports **automation-first execution** +- Preserve old method signatures or module boundaries. +- Build a generic workflow engine. +- Make the reporting graph drive scheduling in v1. +- Implement DNA extraction as a real v1 stage. +- Automatically run Opentrons simulation. +- Silently mutate biological sequences without approval. +- Support arbitrary variable-length level-1 designs in v1. ---- +## V1 scope -## 🔁 Full Workflow Experience +### Design classification -User provides: -- SBOL design -- Available plasmid collections +Initial abstract designs are classified as: -BuildCompiler returns: -- Complete cloning plan -- Robot-ready protocols -- Step-by-step instructions +```text +ModuleDefinition + -> assembly_lvl2 ---- +CombinatorialDerivation + -> expand to concrete assembly_lvl1 variants -## 🧬 Supported Biology +ComponentDefinition with >1 component + -> assembly_lvl1 -- MoClo cloning -- Single-gene builds -- Multi-gene constructs -- Automated strain generation +ComponentDefinition with <=1 component and supported role + -> domestication ---- +ComponentDefinition with <=1 component and unsupported role + -> unsupported planning record +``` -## 🚧 Current Limitations +Supported level-1 roles for v1 are exactly: -- Max 4 genes in Level 2 -- OT-2 only -- MoClo only +```text +promoter, RBS, CDS, terminator +``` ---- +A valid level-1 concrete request contains exactly one promoter, one RBS, one CDS, and one terminator. -## 🔮 Roadmap +### Full-build behavior -- Gibson / Loop assembly -- Multi-robot support -- Cloud lab integration -- LabOP standard support +`full_build` should use a bounded dependency-resolution loop: + +```text +1. Try buildable level-2 requests. +2. Promote missing engineered regions to level-1 requests. +3. Try buildable level-1 requests. +4. Promote missing promoter/RBS/CDS/terminator parts to domestication requests. +5. Try domestication requests. +6. Index generated products. +7. Retry upward until success, no progress, failure, or max_iterations. +``` + +Successful assembly/domestication products are immediately chained to transformation and plating, deduplicated by product identity. + +### Level-2 route optimization + +Level-2 assembly expects a `ModuleDefinition` with engineered regions as components. If `region_order` is supplied, it is a hard constraint. If no order is supplied, the planner searches possible engineered-region orders up to the default exhaustive bound of 4 regions. + +Primary objective: + +```text +minimize number of new level-1 plasmids required +``` + +Tie-breakers: + +1. Respect hard constraints. +2. Prefer existing collection plasmids over generated/planned plasmids. +3. Prefer higher material state. +4. Prefer fewer total assemblies. +5. Tie-break by stable ordered SBOL identities. + +The detailed report should include the selected route and top 3 rejected alternatives. + +### Level-1 route optimization + +Level-1 assembly uses SBOL order when unambiguous. If SBOL order differs from promoter/RBS/CDS/terminator, warn the user but respect SBOL order. If order is ambiguous, enforce canonical promoter/RBS/CDS/terminator order. + +Primary objective: + +```text +minimize number of new domestications required +``` + +Tie-breakers mirror level-2 route selection. + +### Domestication + +Domestication produces a domesticated plasmid as the primary product. Edited inserts and internal BsaI-site edits are provenance/artifacts, not primary products. + +Compiler-only mode may propose sequence edits and record them. Protocol/execution mode must block unless the relevant process is approved. + +### Reporting + +Every `FullBuildResult` includes a lightweight `BuildSummary`. A comprehensive `BuildReport` is generated only when `options.reporting.include_detailed_report=True`. + +The detailed report should include: + +- status +- executive summary +- stage sections +- selected routes +- rejected alternatives +- missing inputs +- required approvals +- warnings +- next immediate actions +- full dependency chain +- graph summary + +## Success criteria + +V1 is successful when: + +- The new architecture exists with the target package boundaries. +- `BuildCompiler.plan(...)` classifies designs and expands valid combinatorial variants. +- `BuildCompiler.execute(...)` runs a bounded dependency loop with mocked/stubbed stages. +- Level-1 assembly is ported behind the new `AssemblyService` interface. +- Level-1 and level-2 route optimizers return selected routes plus structured blockers. +- Domestication reports sequence-edit approvals and missing backbones correctly. +- Compiler-only mode returns SBOL artifacts and PUDU-compatible JSON intermediates without PUDU/Opentrons. +- Transform/plating chaining deduplicates by product identity. +- `FullBuildResult.summary` always exists and `FullBuildResult.report` is opt-in. +- Core tests run under pytest + Ruff without optional automation dependencies. + +## Future considerations + +- DNA extraction as a real material-state transition. +- Larger level-2 search strategies beyond exhaustive order enumeration. +- Variable-length level-1 constructs. +- Persistent approval files. +- Rich SynBioSuite UI integration for route alternatives, build graph, and approvals. +- Stronger SBOL validation and provenance visualization. diff --git a/README.md b/README.md index 4335060..addf87a 100644 --- a/README.md +++ b/README.md @@ -1,156 +1,258 @@ # BuildCompiler - BuildCompiler is an open-source tool that bridges the Design and Build stages of the Synthetic Biology DBTL cycle by compiling standardized genetic designs into executable DNA assembly, transformation, and plating workflows. -It supports build functionality in comand line and cloud workflows in [SynBioSuite](https://synbiosuite.org), based off the [SBOL Best Practices](https://github.com/SynBioDex/SBOL-examples/tree/main/SBOL/best-practices/BP011/). +BuildCompiler is a Python compiler pipeline for synthetic biology build planning. It takes abstract SBOL designs and indexed biological inventory, then produces an executable build plan across domestication, MoClo assembly level 1, MoClo assembly level 2, transformation, and plating. -BuildCompiler light logo -BuildCompiler night logo +This repository is being refactored around a clean architecture. The existing codebase is useful as working evidence, especially the level-1 assembly path and SBOL digestion/ligation behavior, but the new implementation should not preserve old APIs, old import paths, or legacy module boundaries except the root package name `buildcompiler`. -![PyPI - Version](https://img.shields.io/pypi/v/sbol2build) -[![Documentation Status](https://readthedocs.org/projects/sbol2build/badge/?version=latest)](https://sbol2build.readthedocs.io/en/latest/?badge=latest) -![PyPI - Python Version](https://img.shields.io/pypi/pyversions/sbol2build) -![PyPI - License](https://img.shields.io/pypi/l/sbol2build) -![gh-action badge](https://github.com/MyersResearchGroup/sbol2build/workflows/Python%20package/badge.svg) +## Why this exists -# BuildCompiler - -BuildCompiler is an open-source framework that converts **abstract genetic designs** into **fully executable cloning workflows** using MoClo (Golden Gate) assembly. - -It bridges the gap between *design* and *build* in the Design–Build–Test–Learn (DBTL) cycle by automatically generating: - -- DNA assembly plans -- Lab automation protocols (Opentrons OT-2) -- Transformation workflows -- Plating protocols -- Step-by-step execution instructions - -👉 Instead of manually planning cloning experiments, BuildCompiler **compiles them**. +Designing a genetic construct is easier than building it in the lab. BuildCompiler should bridge that gap by compiling: ---- - -## 🧬 What Problem Does It Solve? +```text +abstract SBOL design + inventory -> build plan -> SBOL build artifacts -> PUDU JSON -> optional manual/OT-2 protocols +``` -Designing genetic constructs is easy. -Building them in the lab is not. +The compiler should answer: -BuildCompiler automates the transition from: +- Can this design be built from current inventory? +- Which plasmids, backbones, and reagents are required? +- Which missing engineered regions need level-1 assembly? +- Which missing promoter/RBS/CDS/terminator parts need domestication? +- Which route minimizes new build work? +- Which transformations and plating layouts follow from successful products? +- What actions should a user take next when a build is blocked? -**SBOL design → DNA → cells on plates** +## Core capabilities for v1 -This reduces: -- Manual planning errors -- Protocol design time -- Lab-to-lab variability +- Classify SBOL abstract designs into level-2, level-1, domestication, or unsupported work. +- Plan and execute a bounded full-build dependency loop. +- Support partial success: build what can be built, report what is missing, and retry after generated products are indexed. +- Optimize level-2 routes by searching feasible engineered-region orders and minimizing new level-1 plasmids. +- Optimize level-1 routes by minimizing new domestications. +- Produce cumulative and per-stage SBOL build artifacts. +- Produce in-memory PUDU-compatible JSON intermediates in compiler-only mode. +- Chain successful assembly/domestication products to transformation and plating, deduplicated by product identity. +- Return structured statuses, missing inputs, required approvals, warnings, summaries, and optional detailed reports. +- Keep PUDU protocol generation and Opentrons simulation optional. ---- +## Non-goals for v1 -## ⚙️ Core Capabilities +- Do not preserve legacy APIs or compatibility wrappers. +- Do not implement DNA extraction as a real stage; reserve `extracted` as a future material state. +- Do not run PUDU or Opentrons simulation by default. +- Do not make the build graph the scheduler in v1; it is reporting-only. +- Do not support variable-length level-1 constructs in v1. +- Do not silently approve sequence edits, reagent purchase, large combinatorial expansion, or large level-2 order search. -BuildCompiler provides a complete cloning workflow composed of: +## Architecture at a glance -### 1. Index Collections -Scans SBOL documents to build an internal index of available plasmids. +BuildCompiler should be organized as a compiler pipeline: -### 2. Domestication -Creates missing parts as linear DNA and generates protocols to insert them into plasmids. +```text +api -> planning -> execution -> stages -> sbol/inventory/adapters -> reporting +``` -### 3. Assembly Level 1 (Single Gene) -- Maps abstract parts → plasmids -- Builds a gene using MoClo -- Generates automation-ready protocols +Recommended package layout: -### 4. Assembly Level 2 (Multi-Gene) -- Combines up to 4 genes into a construct -- Uses Level 1 products as inputs +```text +src/buildcompiler/ + __init__.py + + api/ + __init__.py + compiler.py + options.py + + domain/ + __init__.py + build_request.py + build_result.py + missing_input.py + material_state.py + plasmid.py + reagent.py + design.py + approvals.py + warnings.py + + planning/ + __init__.py + classifier.py + combinatorial.py + full_build_planner.py + validation.py + + execution/ + __init__.py + context.py + full_build_executor.py + worklist.py + stage_runner.py + indexing.py + + stages/ + __init__.py + domestication.py + assembly_lvl1.py + assembly_lvl2.py + transformation.py + plating.py + + sbol/ + __init__.py + assembly.py + domestication.py + transformation.py + documents.py + identities.py + resolver.py + validation.py + constants.py + + inventory/ + __init__.py + synbiohub.py + collection_indexer.py + plasmid_index.py + backbone_index.py + reagent_index.py + product_index.py + compatibility.py + selector.py + + adapters/ + __init__.py + pudu/ + __init__.py + assembly_json.py + transformation_json.py + plating_json.py + protocol_generation.py + opentrons/ + __init__.py + simulation.py + + reporting/ + __init__.py + build_graph.py + summaries.py + reports.py + serialization.py + + errors.py + logging.py +``` -### 5. Transformation -- Converts DNA into engineered strains -- Generates automated chemical transformation protocols +## Public API target -### 6. Plating -- Creates dilution series -- Generates plating protocols +Keep the import root `buildcompiler`. -### 7. Full Build (Orchestrator) -Runs the entire workflow automatically: -- Detects missing parts -- Generates them if needed -- Executes all assembly steps -- Chains transformation and plating +Primary usage should be stateful and object-based: ---- +```python +from buildcompiler.api import BuildCompiler -## 🔄 Full Build Workflow +compiler = BuildCompiler.from_synbiohub( + collections=collections, + sbh_registry=sbh_registry, + auth_token=auth_token, + sbol_doc=sbol_doc, +) -```text -SBOL Design - ↓ -Index Collections - ↓ -Domestication - ↓ -Transformation - ↓ -Plating - ↓ -DNA Extraction - ↓ -Assembly Level 1 (Missing parts added to Domestication) - ↓ -Transformation - ↓ -Plating - ↓ -DNA Extraction - ↓ -Assembly Level 2 (Missing parts added to Assembly Level 1) - ↓ -Transformation - ↓ -Plating - ↓ -Final Build Outputs +plan = compiler.plan(abstract_designs) +result = compiler.execute(plan) ``` -## Installing BuildCompiler: -```pip install buildcompiler``` +A convenience wrapper may exist: -## Documentation +```python +from buildcompiler.api import full_build - Please visit the documentation with API reference and tutorials at Read the Docs: [sbol2build.rtfd.io](https://sbol2build.readthedocs.io) - -## Environment Setup +result = full_build( + abstract_designs=abstract_designs, + collections=collections, + sbh_registry=sbh_registry, + auth_token=auth_token, + sbol_doc=sbol_doc, +) +``` -If you are interested in contributing to **BuildCompiler**, please set up your local development environment with the same tools used in CI and linting. +`BuildCompiler.__init__` should stay lightweight and dependency-injected. Automatic SynBioHub collection indexing belongs in `BuildCompiler.from_synbiohub(...)`. -### 1. Install [uv](https://docs.astral.sh/uv/) +## Local development -`uv` manages all Python dependencies (including dev tools) with a lockfile for reproducibility. +Recommended local workflow: -#### Linux/Bash ```bash -curl -LsSf https://astral.sh/uv/install.sh | sh +uv sync --all-groups +uv run ruff check . +uv run ruff format --check . +uv run pytest ``` -#### Mac OSX with Homebrew + +If `uv` is not available, use a normal virtual environment and install editable dependencies: + ```bash -brew install uv +python -m venv .venv +source .venv/bin/activate +python -m pip install -U pip +python -m pip install -e '.[test]' +python -m pip install ruff +ruff check . +ruff format --check . +pytest ``` -### 2. Sync dependencies + +Automation-specific tests should be optional: + ```bash -uv sync --all-groups +python -m pip install -e '.[automation,test]' +pytest tests/automation ``` -This will create a virtual environment with the dependiencies. Activate using: + +## Container workflow + +A Docker Compose workflow is recommended for reliable Codex and contributor development, but it does not need to block the first implementation PR. + +Target commands after Docker support exists: + ```bash -source .venv/bin/activate +docker compose build +docker compose run --rm app ruff check . +docker compose run --rm app ruff format --check . +docker compose run --rm app pytest ``` -### 3. Install pre-commit hooks -We use pre-commit to automatically run the Ruff linter before every commit. -Install and enable the hooks with: +Core CI should not require PUDU or Opentrons. Those dependencies are optional and should live behind optional test jobs or manual workflows. + +## Testing and quality checks + +Default CI should run: + ```bash -uv run pre-commit install +ruff check . +ruff format --check . +pytest tests/unit tests/stages tests/integration ``` +Testing priorities: + +1. Domain dataclasses and status semantics. +2. Planning and design classification. +3. Inventory indexing and compatibility selection. +4. Level-1 and level-2 route optimizers. +5. Domestication sequence-edit approval behavior. +6. Full-build bounded retry loop with mocked stages. +7. SBOL assembly service port using existing fixtures. +8. Transformation and plating deduplication. +9. Summary/report/graph generation. +10. Optional PUDU/Opentrons adapter smoke tests. + +## How ChatGPT and Codex should use these docs + +- `PRODUCT.md` defines the product intent, v1 scope, and non-goals. +- `ARCHITECTURE.md` defines module boundaries and implementation contracts. +- `AGENT.md` defines how Codex should take tasks, report progress, and escalate decisions. +- `ADR-001.md` records the clean-architecture rewrite decision and its tradeoffs. -#### Running tests: -`uv run python -m unittest discover -s tests` +Codex should treat these files as the source of truth for implementation unless the user or ChatGPT explicitly revises them. From 21271fbdf964dd1f01a8b62c2493219dda649312 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 08:41:25 -0600 Subject: [PATCH 38/93] Add clean-architecture package scaffolding for issue #53 --- src/buildcompiler/adapters/__init__.py | 1 + src/buildcompiler/adapters/opentrons/__init__.py | 1 + src/buildcompiler/adapters/pudu/__init__.py | 1 + src/buildcompiler/api/__init__.py | 1 + src/buildcompiler/domain/__init__.py | 1 + src/buildcompiler/execution/__init__.py | 1 + src/buildcompiler/inventory/__init__.py | 1 + src/buildcompiler/planning/__init__.py | 1 + src/buildcompiler/reporting/__init__.py | 1 + src/buildcompiler/sbol/__init__.py | 1 + src/buildcompiler/stages/__init__.py | 1 + tests/test_package_scaffolding.py | 15 +++++++++++++++ 12 files changed, 26 insertions(+) create mode 100644 src/buildcompiler/adapters/__init__.py create mode 100644 src/buildcompiler/adapters/opentrons/__init__.py create mode 100644 src/buildcompiler/adapters/pudu/__init__.py create mode 100644 src/buildcompiler/api/__init__.py create mode 100644 src/buildcompiler/domain/__init__.py create mode 100644 src/buildcompiler/execution/__init__.py create mode 100644 src/buildcompiler/inventory/__init__.py create mode 100644 src/buildcompiler/planning/__init__.py create mode 100644 src/buildcompiler/reporting/__init__.py create mode 100644 src/buildcompiler/sbol/__init__.py create mode 100644 src/buildcompiler/stages/__init__.py create mode 100644 tests/test_package_scaffolding.py diff --git a/src/buildcompiler/adapters/__init__.py b/src/buildcompiler/adapters/__init__.py new file mode 100644 index 0000000..5e19692 --- /dev/null +++ b/src/buildcompiler/adapters/__init__.py @@ -0,0 +1 @@ +"""Package scaffolding for clean architecture.""" diff --git a/src/buildcompiler/adapters/opentrons/__init__.py b/src/buildcompiler/adapters/opentrons/__init__.py new file mode 100644 index 0000000..5e19692 --- /dev/null +++ b/src/buildcompiler/adapters/opentrons/__init__.py @@ -0,0 +1 @@ +"""Package scaffolding for clean architecture.""" diff --git a/src/buildcompiler/adapters/pudu/__init__.py b/src/buildcompiler/adapters/pudu/__init__.py new file mode 100644 index 0000000..5e19692 --- /dev/null +++ b/src/buildcompiler/adapters/pudu/__init__.py @@ -0,0 +1 @@ +"""Package scaffolding for clean architecture.""" diff --git a/src/buildcompiler/api/__init__.py b/src/buildcompiler/api/__init__.py new file mode 100644 index 0000000..5e19692 --- /dev/null +++ b/src/buildcompiler/api/__init__.py @@ -0,0 +1 @@ +"""Package scaffolding for clean architecture.""" diff --git a/src/buildcompiler/domain/__init__.py b/src/buildcompiler/domain/__init__.py new file mode 100644 index 0000000..5e19692 --- /dev/null +++ b/src/buildcompiler/domain/__init__.py @@ -0,0 +1 @@ +"""Package scaffolding for clean architecture.""" diff --git a/src/buildcompiler/execution/__init__.py b/src/buildcompiler/execution/__init__.py new file mode 100644 index 0000000..5e19692 --- /dev/null +++ b/src/buildcompiler/execution/__init__.py @@ -0,0 +1 @@ +"""Package scaffolding for clean architecture.""" diff --git a/src/buildcompiler/inventory/__init__.py b/src/buildcompiler/inventory/__init__.py new file mode 100644 index 0000000..5e19692 --- /dev/null +++ b/src/buildcompiler/inventory/__init__.py @@ -0,0 +1 @@ +"""Package scaffolding for clean architecture.""" diff --git a/src/buildcompiler/planning/__init__.py b/src/buildcompiler/planning/__init__.py new file mode 100644 index 0000000..5e19692 --- /dev/null +++ b/src/buildcompiler/planning/__init__.py @@ -0,0 +1 @@ +"""Package scaffolding for clean architecture.""" diff --git a/src/buildcompiler/reporting/__init__.py b/src/buildcompiler/reporting/__init__.py new file mode 100644 index 0000000..5e19692 --- /dev/null +++ b/src/buildcompiler/reporting/__init__.py @@ -0,0 +1 @@ +"""Package scaffolding for clean architecture.""" diff --git a/src/buildcompiler/sbol/__init__.py b/src/buildcompiler/sbol/__init__.py new file mode 100644 index 0000000..5e19692 --- /dev/null +++ b/src/buildcompiler/sbol/__init__.py @@ -0,0 +1 @@ +"""Package scaffolding for clean architecture.""" diff --git a/src/buildcompiler/stages/__init__.py b/src/buildcompiler/stages/__init__.py new file mode 100644 index 0000000..5e19692 --- /dev/null +++ b/src/buildcompiler/stages/__init__.py @@ -0,0 +1 @@ +"""Package scaffolding for clean architecture.""" diff --git a/tests/test_package_scaffolding.py b/tests/test_package_scaffolding.py new file mode 100644 index 0000000..c8b9ec6 --- /dev/null +++ b/tests/test_package_scaffolding.py @@ -0,0 +1,15 @@ +"""Import smoke tests for clean-architecture package scaffolding.""" + + +def test_clean_architecture_package_imports() -> None: + import buildcompiler.api # noqa: F401 + import buildcompiler.domain # noqa: F401 + import buildcompiler.planning # noqa: F401 + import buildcompiler.execution # noqa: F401 + import buildcompiler.stages # noqa: F401 + import buildcompiler.sbol # noqa: F401 + import buildcompiler.inventory # noqa: F401 + import buildcompiler.adapters # noqa: F401 + import buildcompiler.adapters.pudu # noqa: F401 + import buildcompiler.adapters.opentrons # noqa: F401 + import buildcompiler.reporting # noqa: F401 From c843d6418dada8d712753ea38084e0fc343ebbb5 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 09:17:01 -0600 Subject: [PATCH 39/93] Add domain enums and result dataclass contracts for full_build --- src/buildcompiler/domain/__init__.py | 35 ++++++++- src/buildcompiler/domain/approvals.py | 22 ++++++ src/buildcompiler/domain/build_request.py | 21 ++++++ src/buildcompiler/domain/build_result.py | 50 +++++++++++++ src/buildcompiler/domain/build_stage.py | 13 ++++ src/buildcompiler/domain/design.py | 12 ++++ src/buildcompiler/domain/material_state.py | 13 ++++ src/buildcompiler/domain/missing_input.py | 32 +++++++++ src/buildcompiler/domain/plasmid.py | 30 ++++++++ src/buildcompiler/domain/reagent.py | 15 ++++ src/buildcompiler/domain/status.py | 25 +++++++ src/buildcompiler/domain/warnings.py | 17 +++++ tests/unit/domain/test_contracts.py | 84 ++++++++++++++++++++++ 13 files changed, 368 insertions(+), 1 deletion(-) create mode 100644 src/buildcompiler/domain/approvals.py create mode 100644 src/buildcompiler/domain/build_request.py create mode 100644 src/buildcompiler/domain/build_result.py create mode 100644 src/buildcompiler/domain/build_stage.py create mode 100644 src/buildcompiler/domain/design.py create mode 100644 src/buildcompiler/domain/material_state.py create mode 100644 src/buildcompiler/domain/missing_input.py create mode 100644 src/buildcompiler/domain/plasmid.py create mode 100644 src/buildcompiler/domain/reagent.py create mode 100644 src/buildcompiler/domain/status.py create mode 100644 src/buildcompiler/domain/warnings.py create mode 100644 tests/unit/domain/test_contracts.py diff --git a/src/buildcompiler/domain/__init__.py b/src/buildcompiler/domain/__init__.py index 5e19692..72eacbb 100644 --- a/src/buildcompiler/domain/__init__.py +++ b/src/buildcompiler/domain/__init__.py @@ -1 +1,34 @@ -"""Package scaffolding for clean architecture.""" +"""Domain contracts for BuildCompiler clean architecture.""" + +from .approvals import ApprovalStatus, RequiredApproval +from .build_request import BuildRequest +from .build_result import FullBuildResult, StageResult +from .build_stage import BuildStage +from .design import DesignKind +from .material_state import MaterialState +from .missing_input import MissingBuildInput +from .plasmid import IndexedBackbone, IndexedPlasmid +from .reagent import IndexedReagent +from .status import BuildStatus, StageStatus +from .warnings import BuildWarning + +__all__ = [ + "ApprovalStatus", + "BuildRequest", + "BuildResult", + "BuildStage", + "BuildStatus", + "BuildWarning", + "DesignKind", + "FullBuildResult", + "IndexedBackbone", + "IndexedPlasmid", + "IndexedReagent", + "MaterialState", + "MissingBuildInput", + "RequiredApproval", + "StageResult", + "StageStatus", +] + +BuildResult = FullBuildResult diff --git a/src/buildcompiler/domain/approvals.py b/src/buildcompiler/domain/approvals.py new file mode 100644 index 0000000..2ebbd45 --- /dev/null +++ b/src/buildcompiler/domain/approvals.py @@ -0,0 +1,22 @@ +"""Approval contracts for expected gated processes.""" + +from dataclasses import dataclass, field +from enum import Enum +from typing import Any + + +class ApprovalStatus(str, Enum): + """Minimal approval state used by RequiredApproval.""" + + REQUIRED = "required" + APPROVED = "approved" + + +@dataclass +class RequiredApproval: + """Approval record for a process required to proceed.""" + + status: ApprovalStatus + process: str + reason: str + metadata: dict[str, Any] = field(default_factory=dict) diff --git a/src/buildcompiler/domain/build_request.py b/src/buildcompiler/domain/build_request.py new file mode 100644 index 0000000..8bcd9c4 --- /dev/null +++ b/src/buildcompiler/domain/build_request.py @@ -0,0 +1,21 @@ +"""Build request contract dataclass.""" + +from dataclasses import dataclass, field +from typing import Any + +from .build_stage import BuildStage +from .design import DesignKind + + +@dataclass +class BuildRequest: + """Planner-produced request for a single stage/source item.""" + + id: str + stage: BuildStage + source_identity: str + source_display_id: str | None + source_kind: DesignKind + parent_group: str | None = None + variant_index: int | None = None + constraints: dict[str, Any] = field(default_factory=dict) diff --git a/src/buildcompiler/domain/build_result.py b/src/buildcompiler/domain/build_result.py new file mode 100644 index 0000000..bc252cb --- /dev/null +++ b/src/buildcompiler/domain/build_result.py @@ -0,0 +1,50 @@ +"""Stage/full-build result contracts.""" + +from dataclasses import dataclass, field +from typing import Any + +from .approvals import RequiredApproval +from .build_stage import BuildStage +from .missing_input import MissingBuildInput +from .plasmid import IndexedPlasmid +from .status import BuildStatus, StageStatus +from .warnings import BuildWarning + + +@dataclass +class StageResult: + """Output contract from a single stage invocation.""" + + id: str + stage: BuildStage + status: StageStatus + request_ids: list[str] = field(default_factory=list) + products: list[IndexedPlasmid] = field(default_factory=list) + missing_inputs: list[MissingBuildInput] = field(default_factory=list) + required_approvals: list[RequiredApproval] = field(default_factory=list) + warnings: list[BuildWarning] = field(default_factory=list) + sbol_document: Any | None = None + json_intermediate: dict[str, Any] | list[Any] | None = None + protocol_artifacts: dict[str, Any] = field(default_factory=dict) + logs: list[str] = field(default_factory=list) + + +@dataclass +class FullBuildResult: + """Aggregate full-build output contract. + + Neighboring contracts (plan/graph/summary/report) are intentionally typed + conservatively until their milestone implementations are added. + """ + + status: BuildStatus + plan: Any + build_document: Any + stage_results: list[StageResult] = field(default_factory=list) + graph: Any = None + final_products: list[IndexedPlasmid] = field(default_factory=list) + missing_inputs: list[MissingBuildInput] = field(default_factory=list) + required_approvals: list[RequiredApproval] = field(default_factory=list) + warnings: list[BuildWarning] = field(default_factory=list) + summary: Any = None + report: Any | None = None diff --git a/src/buildcompiler/domain/build_stage.py b/src/buildcompiler/domain/build_stage.py new file mode 100644 index 0000000..a3b52a9 --- /dev/null +++ b/src/buildcompiler/domain/build_stage.py @@ -0,0 +1,13 @@ +"""Build stage enum contracts.""" + +from enum import Enum + + +class BuildStage(str, Enum): + """Planned v1 build stages for full-build execution.""" + + DOMESTICATION = "domestication" + ASSEMBLY_LVL1 = "assembly_lvl1" + ASSEMBLY_LVL2 = "assembly_lvl2" + TRANSFORMATION = "transformation" + PLATING = "plating" diff --git a/src/buildcompiler/domain/design.py b/src/buildcompiler/domain/design.py new file mode 100644 index 0000000..c11bedc --- /dev/null +++ b/src/buildcompiler/domain/design.py @@ -0,0 +1,12 @@ +"""Design identity/type contracts.""" + +from enum import Enum + + +class DesignKind(str, Enum): + """Supported SBOL design source kinds for build requests.""" + + COMPONENT_DEFINITION = "component_definition" + MODULE_DEFINITION = "module_definition" + COMBINATORIAL_DERIVATION = "combinatorial_derivation" + UNSUPPORTED = "unsupported" diff --git a/src/buildcompiler/domain/material_state.py b/src/buildcompiler/domain/material_state.py new file mode 100644 index 0000000..ea5b08e --- /dev/null +++ b/src/buildcompiler/domain/material_state.py @@ -0,0 +1,13 @@ +"""Material lifecycle states used across stages.""" + +from enum import Enum + + +class MaterialState(str, Enum): + """Normalized lifecycle states for build materials.""" + + PLANNED = "planned" + GENERATED = "generated" + ASSEMBLED = "assembled" + TRANSFORMED = "transformed" + PLATED = "plated" diff --git a/src/buildcompiler/domain/missing_input.py b/src/buildcompiler/domain/missing_input.py new file mode 100644 index 0000000..1210eb9 --- /dev/null +++ b/src/buildcompiler/domain/missing_input.py @@ -0,0 +1,32 @@ +"""Missing input/blocker contracts.""" + +from dataclasses import dataclass, field +from typing import Literal + +from .build_stage import BuildStage + +MissingKind = Literal[ + "engineered_region", + "promoter", + "rbs", + "cds", + "terminator", + "backbone", + "restriction_enzyme", + "ligase", + "reagent", +] + + +@dataclass +class MissingBuildInput: + """Expected blocker produced when build inputs are unavailable.""" + + source_stage: BuildStage + source_design_identity: str + missing_identity: str + missing_display_id: str | None + missing_kind: MissingKind + required_stage: BuildStage | Literal["fatal"] + reason: str + candidates_tried: list[str] = field(default_factory=list) diff --git a/src/buildcompiler/domain/plasmid.py b/src/buildcompiler/domain/plasmid.py new file mode 100644 index 0000000..3a2e0f7 --- /dev/null +++ b/src/buildcompiler/domain/plasmid.py @@ -0,0 +1,30 @@ +"""Normalized plasmid/backbone records.""" + +from dataclasses import dataclass, field +from typing import Any + +from .material_state import MaterialState + + +@dataclass +class IndexedPlasmid: + """Inventory/index record for a plasmid-like material.""" + + identity: str + display_id: str | None = None + name: str | None = None + state: MaterialState = MaterialState.PLANNED + roles: list[str] = field(default_factory=list) + metadata: dict[str, Any] = field(default_factory=dict) + sbol_component: Any | None = None + + +@dataclass +class IndexedBackbone: + """Inventory/index record for a backbone material.""" + + identity: str + display_id: str | None = None + name: str | None = None + metadata: dict[str, Any] = field(default_factory=dict) + sbol_component: Any | None = None diff --git a/src/buildcompiler/domain/reagent.py b/src/buildcompiler/domain/reagent.py new file mode 100644 index 0000000..b50e690 --- /dev/null +++ b/src/buildcompiler/domain/reagent.py @@ -0,0 +1,15 @@ +"""Normalized reagent record contracts.""" + +from dataclasses import dataclass, field +from typing import Any + + +@dataclass +class IndexedReagent: + """Inventory/index record for reagents.""" + + identity: str + display_id: str | None = None + name: str | None = None + reagent_type: str | None = None + metadata: dict[str, Any] = field(default_factory=dict) diff --git a/src/buildcompiler/domain/status.py b/src/buildcompiler/domain/status.py new file mode 100644 index 0000000..aac2ba3 --- /dev/null +++ b/src/buildcompiler/domain/status.py @@ -0,0 +1,25 @@ +"""Status enums and contract-level semantics helpers.""" + +from enum import Enum + + +class StageStatus(str, Enum): + """Status for a single stage result. + + BLOCKED means expected inputs or approvals can unblock this stage later. + FAILED means the request cannot proceed without changing design/options/ + collections/approval state. + """ + + SUCCESS = "success" + PARTIAL_SUCCESS = "partial_success" + BLOCKED = "blocked" + FAILED = "failed" + + +class BuildStatus(str, Enum): + """Status for full-build aggregate results.""" + + SUCCESS = "success" + PARTIAL_SUCCESS = "partial_success" + FAILED = "failed" diff --git a/src/buildcompiler/domain/warnings.py b/src/buildcompiler/domain/warnings.py new file mode 100644 index 0000000..72f130d --- /dev/null +++ b/src/buildcompiler/domain/warnings.py @@ -0,0 +1,17 @@ +"""Domain warning contracts.""" + +from dataclasses import dataclass, field +from typing import Any + +from .build_stage import BuildStage + + +@dataclass +class BuildWarning: + """Structured non-fatal warning for planning/execution/reporting.""" + + code: str + message: str + stage: BuildStage | None = None + source_identity: str | None = None + metadata: dict[str, Any] = field(default_factory=dict) diff --git a/tests/unit/domain/test_contracts.py b/tests/unit/domain/test_contracts.py new file mode 100644 index 0000000..e85183e --- /dev/null +++ b/tests/unit/domain/test_contracts.py @@ -0,0 +1,84 @@ +from buildcompiler.domain import ( + ApprovalStatus, + BuildRequest, + BuildStage, + BuildStatus, + BuildWarning, + DesignKind, + MissingBuildInput, + RequiredApproval, + StageResult, + StageStatus, +) + + +def test_enum_values_and_planned_stages(): + assert StageStatus.BLOCKED.value == "blocked" + assert StageStatus.FAILED.value == "failed" + assert BuildStatus.SUCCESS.value == "success" + assert BuildStage.ASSEMBLY_LVL1.value == "assembly_lvl1" + assert BuildStage.DOMESTICATION.value == "domestication" + assert BuildStage.ASSEMBLY_LVL2.value == "assembly_lvl2" + assert BuildStage.TRANSFORMATION.value == "transformation" + assert BuildStage.PLATING.value == "plating" + + +def test_blocked_vs_failed_semantics_are_documented_as_contract_data(): + blocked = MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL1, + source_design_identity="sbol://design/x", + missing_identity="sbol://part/y", + missing_display_id="y", + missing_kind="cds", + required_stage=BuildStage.DOMESTICATION, + reason="Input could be generated by upstream stage", + ) + failed = MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL1, + source_design_identity="sbol://design/x", + missing_identity="sbol://part/z", + missing_display_id="z", + missing_kind="cds", + required_stage="fatal", + reason="Unsupported design cannot proceed without changes", + ) + + assert blocked.required_stage != "fatal" + assert failed.required_stage == "fatal" + + +def test_default_mutables_are_isolated_across_instances(): + req1 = BuildRequest("r1", BuildStage.DOMESTICATION, "a", None, DesignKind.COMPONENT_DEFINITION) + req2 = BuildRequest("r2", BuildStage.DOMESTICATION, "b", None, DesignKind.COMPONENT_DEFINITION) + req1.constraints["x"] = 1 + assert req2.constraints == {} + + s1 = StageResult("s1", BuildStage.DOMESTICATION, StageStatus.SUCCESS) + s2 = StageResult("s2", BuildStage.DOMESTICATION, StageStatus.SUCCESS) + s1.request_ids.append("r1") + s1.products.append(object()) + s1.missing_inputs.append( + MissingBuildInput(BuildStage.DOMESTICATION, "a", "b", None, "promoter", "fatal", "missing") + ) + s1.required_approvals.append(RequiredApproval(ApprovalStatus.REQUIRED, "biosafety", "needed")) + s1.warnings.append(BuildWarning("warn", "msg")) + s1.protocol_artifacts["x"] = "y" + s1.logs.append("log") + + assert s2.request_ids == [] + assert s2.products == [] + assert s2.missing_inputs == [] + assert s2.required_approvals == [] + assert s2.warnings == [] + assert s2.protocol_artifacts == {} + assert s2.logs == [] + + a1 = RequiredApproval(ApprovalStatus.REQUIRED, "biosafety", "needed") + a2 = RequiredApproval(ApprovalStatus.APPROVED, "biosafety", "granted") + a1.metadata["id"] = "1" + assert a2.metadata == {} + + w1 = BuildWarning("w1", "warning") + w2 = BuildWarning("w2", "warning") + w1.metadata["code"] = "X" + assert w2.metadata == {} From f16874b146efe9e76d1569ed4a9f23c892118a72 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 10:33:15 -0600 Subject: [PATCH 40/93] Raise declared minimum Python version to 3.10 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a0c13bb..f7c72fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "synbio-buildcompiler" version = "0.0.a1" description = "BuildCompiler is an open-source tool that bridges the Design and Build stages of the Synthetic Biology DBTL cycle" readme = "README.md" -requires-python = ">=3.7" +requires-python = ">=3.10" license = {file = "LICENSE.md"} keywords = ["SBOL", "genetic", "automation", "build", "synthetic biology"] authors = [ From 9a475bda095683a7516885c88864710112f80bab Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 10:45:43 -0600 Subject: [PATCH 41/93] Add composed BuildOptions API dataclasses --- src/buildcompiler/api/__init__.py | 32 ++++++++++- src/buildcompiler/api/options.py | 89 +++++++++++++++++++++++++++++++ tests/unit/api/test_options.py | 37 +++++++++++++ 3 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 src/buildcompiler/api/options.py create mode 100644 tests/unit/api/test_options.py diff --git a/src/buildcompiler/api/__init__.py b/src/buildcompiler/api/__init__.py index 5e19692..3eb0c63 100644 --- a/src/buildcompiler/api/__init__.py +++ b/src/buildcompiler/api/__init__.py @@ -1 +1,31 @@ -"""Package scaffolding for clean architecture.""" +"""Public API contracts and options for BuildCompiler.""" + +from .options import ( + ApprovalOptions, + BuildOptions, + CombinatorialOptions, + DomesticationOptions, + ExecutionOptions, + Lvl2SearchOptions, + PlanningOptions, + ProtocolMode, + ProtocolOptions, + ReagentOptions, + ReportingOptions, + SelectionOptions, +) + +__all__ = [ + "ApprovalOptions", + "BuildOptions", + "CombinatorialOptions", + "DomesticationOptions", + "ExecutionOptions", + "Lvl2SearchOptions", + "PlanningOptions", + "ProtocolMode", + "ProtocolOptions", + "ReagentOptions", + "ReportingOptions", + "SelectionOptions", +] diff --git a/src/buildcompiler/api/options.py b/src/buildcompiler/api/options.py new file mode 100644 index 0000000..5e62868 --- /dev/null +++ b/src/buildcompiler/api/options.py @@ -0,0 +1,89 @@ +"""Build options contracts for full_build configuration.""" + +from dataclasses import dataclass, field +from enum import Enum +from pathlib import Path +from typing import Literal + + +class ProtocolMode(str, Enum): + """Protocol generation mode.""" + + NONE = "none" + MANUAL = "manual" + AUTOMATED = "automated" + + +@dataclass +class CombinatorialOptions: + max_variants: int = 256 + allow_large_expansion: bool = False + + +@dataclass +class Lvl2SearchOptions: + max_exhaustive_region_count: int = 4 + allow_large_order_search: bool = False + + +@dataclass +class PlanningOptions: + combinatorial: CombinatorialOptions = field(default_factory=CombinatorialOptions) + lvl2_search: Lvl2SearchOptions = field(default_factory=Lvl2SearchOptions) + + +@dataclass +class ExecutionOptions: + max_iterations: int = 5 + continue_on_error: bool = False + + +@dataclass +class SelectionOptions: + prefer_existing_collection_material: bool = True + prefer_higher_material_state: bool = True + + +@dataclass +class ProtocolOptions: + mode: ProtocolMode = ProtocolMode.NONE + simulate: bool = False + results_dir: str | Path | None = None + + +@dataclass +class ReportingOptions: + include_detailed_report: bool = False + include_rejected_routes: bool = True + max_rejected_routes: int = 3 + + +@dataclass +class ApprovalOptions: + approved_processes: set[str] = field(default_factory=set) + approved_approval_ids: set[str] = field(default_factory=set) + scope: Literal["run", "persistent"] = "run" + + +@dataclass +class ReagentOptions: + allow_reagent_purchase: bool = False + default_restriction_enzyme: str = "BsaI" + default_ligase: str = "T4_DNA_ligase" + + +@dataclass +class DomesticationOptions: + allow_sequence_domestication_edits: bool = False + + +@dataclass +class BuildOptions: + planning: PlanningOptions = field(default_factory=PlanningOptions) + execution: ExecutionOptions = field(default_factory=ExecutionOptions) + selection: SelectionOptions = field(default_factory=SelectionOptions) + protocol: ProtocolOptions = field(default_factory=ProtocolOptions) + reporting: ReportingOptions = field(default_factory=ReportingOptions) + approvals: ApprovalOptions = field(default_factory=ApprovalOptions) + reagents: ReagentOptions = field(default_factory=ReagentOptions) + domestication: DomesticationOptions = field(default_factory=DomesticationOptions) diff --git a/tests/unit/api/test_options.py b/tests/unit/api/test_options.py new file mode 100644 index 0000000..4ef7282 --- /dev/null +++ b/tests/unit/api/test_options.py @@ -0,0 +1,37 @@ +from buildcompiler.api import BuildOptions, ProtocolMode + + +def test_build_options_defaults_match_contract(): + options = BuildOptions() + + assert options.execution.max_iterations == 5 + assert options.execution.continue_on_error is False + assert options.protocol.mode == ProtocolMode.NONE + assert options.protocol.simulate is False + assert options.reagents.allow_reagent_purchase is False + assert options.reagents.default_restriction_enzyme == "BsaI" + assert options.reagents.default_ligase == "T4_DNA_ligase" + assert options.domestication.allow_sequence_domestication_edits is False + assert options.planning.combinatorial.max_variants == 256 + assert options.planning.combinatorial.allow_large_expansion is False + assert options.planning.lvl2_search.max_exhaustive_region_count == 4 + assert options.planning.lvl2_search.allow_large_order_search is False + assert options.reporting.include_rejected_routes is True + assert options.reporting.max_rejected_routes == 3 + assert options.approvals.approved_processes == set() + assert options.approvals.approved_approval_ids == set() + + +def test_mutable_defaults_are_isolated_across_instances(): + left = BuildOptions() + right = BuildOptions() + + left.approvals.approved_processes.add("biosafety") + left.approvals.approved_approval_ids.add("approval-1") + left.planning.combinatorial.max_variants = 1024 + left.reporting.max_rejected_routes = 10 + + assert right.approvals.approved_processes == set() + assert right.approvals.approved_approval_ids == set() + assert right.planning.combinatorial.max_variants == 256 + assert right.reporting.max_rejected_routes == 3 From 4b3298cea694b4bf3d7e16e4a4688ca6c7495f43 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 11:00:55 -0600 Subject: [PATCH 42/93] Add BuildCompiler API skeleton facade --- src/buildcompiler/api/__init__.py | 5 +- src/buildcompiler/api/compiler.py | 115 ++++++++++++++++++++++++ tests/unit/api/test_compiler_api.py | 132 ++++++++++++++++++++++++++++ 3 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 src/buildcompiler/api/compiler.py create mode 100644 tests/unit/api/test_compiler_api.py diff --git a/src/buildcompiler/api/__init__.py b/src/buildcompiler/api/__init__.py index 3eb0c63..fc04f2b 100644 --- a/src/buildcompiler/api/__init__.py +++ b/src/buildcompiler/api/__init__.py @@ -1,5 +1,6 @@ -"""Public API contracts and options for BuildCompiler.""" +"""Public API contracts, options, and compiler facade for BuildCompiler.""" +from .compiler import BuildCompiler, full_build from .options import ( ApprovalOptions, BuildOptions, @@ -16,6 +17,7 @@ ) __all__ = [ + "BuildCompiler", "ApprovalOptions", "BuildOptions", "CombinatorialOptions", @@ -28,4 +30,5 @@ "ReagentOptions", "ReportingOptions", "SelectionOptions", + "full_build", ] diff --git a/src/buildcompiler/api/compiler.py b/src/buildcompiler/api/compiler.py new file mode 100644 index 0000000..2823293 --- /dev/null +++ b/src/buildcompiler/api/compiler.py @@ -0,0 +1,115 @@ +"""Public BuildCompiler API skeleton.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +from .options import BuildOptions + + +@dataclass +class BuildCompiler: + """Lightweight dependency-injected compiler facade.""" + + inventory: Any = None + sbol_document: Any = None + planner: Any = None + executor: Any = None + adapters: Any = None + options: BuildOptions = field(default_factory=BuildOptions) + + @classmethod + def from_synbiohub( + cls, + *, + collections: list[str] | None = None, + sbh_registry: str | None = None, + auth_token: str | None = None, + sbol_doc: Any = None, + options: BuildOptions | None = None, + **kwargs: Any, + ) -> "BuildCompiler": + """Factory boundary reserved for future SynBioHub loading/indexing.""" + if collections: + raise NotImplementedError( + "Automatic SynBioHub collection loading/indexing is not implemented yet. " + "Inject inventory dependencies directly for now." + ) + + return cls( + sbol_document=sbol_doc, + options=options or BuildOptions(), + **kwargs, + ) + + def plan(self, abstract_designs: Any, options: BuildOptions | None = None) -> Any: + """Plan a build request via injected planner (placeholder in skeleton).""" + if self.planner is None: + raise NotImplementedError( + "Build planning is not implemented in the API skeleton. " + "Inject a planner dependency to use BuildCompiler.plan()." + ) + + effective_options = options or self.options + return self.planner.plan(abstract_designs, options=effective_options) + + def execute(self, plan: Any, options: BuildOptions | None = None) -> Any: + """Execute a build plan via injected executor (placeholder in skeleton).""" + if self.executor is None: + raise NotImplementedError( + "Build execution is not implemented in the API skeleton. " + "Inject an executor dependency to use BuildCompiler.execute()." + ) + + effective_options = options or self.options + return self.executor.execute(plan, options=effective_options) + + def full_build(self, abstract_designs: Any, options: BuildOptions | None = None) -> Any: + """Convenience skeleton method: plan then execute.""" + plan = self.plan(abstract_designs, options=options) + return self.execute(plan, options=options) + + +def full_build( + abstract_designs: Any, + *, + inventory: Any = None, + sbol_document: Any = None, + planner: Any = None, + executor: Any = None, + adapters: Any = None, + options: BuildOptions | None = None, + collections: list[str] | None = None, + sbh_registry: str | None = None, + auth_token: str | None = None, + sbol_doc: Any = None, + **kwargs: Any, +) -> Any: + """Module-level full-build entry point for the public API skeleton.""" + compiler_options = options or BuildOptions() + + if collections is not None or sbh_registry is not None or auth_token is not None or sbol_doc is not None: + compiler = BuildCompiler.from_synbiohub( + collections=collections, + sbh_registry=sbh_registry, + auth_token=auth_token, + sbol_doc=sbol_doc, + options=compiler_options, + inventory=inventory, + planner=planner, + executor=executor, + adapters=adapters, + **kwargs, + ) + else: + compiler = BuildCompiler( + inventory=inventory, + planner=planner, + executor=executor, + adapters=adapters, + options=compiler_options, + **kwargs, + ) + + return compiler.full_build(abstract_designs, options=compiler_options) diff --git a/tests/unit/api/test_compiler_api.py b/tests/unit/api/test_compiler_api.py new file mode 100644 index 0000000..23a34d6 --- /dev/null +++ b/tests/unit/api/test_compiler_api.py @@ -0,0 +1,132 @@ +import sys + +import pytest + +from buildcompiler.api import BuildCompiler, BuildOptions, full_build + + +class FakePlanner: + def __init__(self): + self.calls = [] + + def plan(self, abstract_designs, *, options): + self.calls.append((abstract_designs, options)) + return {"plan": abstract_designs, "options": options} + + +class FakeExecutor: + def __init__(self): + self.calls = [] + + def execute(self, plan, *, options): + self.calls.append((plan, options)) + return {"result": plan, "options": options} + + +def test_import_smoke(): + assert BuildCompiler is not None + assert full_build is not None + assert BuildOptions is not None + + +def test_constructor_defaults_and_injected_dependencies(): + compiler = BuildCompiler() + assert compiler.inventory is None + assert compiler.sbol_document is None + assert compiler.planner is None + assert compiler.executor is None + assert compiler.adapters is None + assert isinstance(compiler.options, BuildOptions) + + inventory = object() + sbol_document = object() + planner = object() + executor = object() + adapters = object() + + injected = BuildCompiler( + inventory=inventory, + sbol_document=sbol_document, + planner=planner, + executor=executor, + adapters=adapters, + ) + + assert injected.inventory is inventory + assert injected.sbol_document is sbol_document + assert injected.planner is planner + assert injected.executor is executor + assert injected.adapters is adapters + + +def test_api_import_does_not_load_optional_automation_modules(): + assert "pudupy" not in sys.modules + assert "opentrons" not in sys.modules + assert "SBOLInventory" not in sys.modules + + +def test_from_synbiohub_placeholder_without_collection_loading(): + compiler = BuildCompiler.from_synbiohub( + collections=[], + sbh_registry=None, + auth_token=None, + sbol_doc=None, + ) + assert isinstance(compiler, BuildCompiler) + + +def test_from_synbiohub_raises_when_collection_loading_is_requested(): + with pytest.raises(NotImplementedError, match="collection loading/indexing"): + BuildCompiler.from_synbiohub(collections=["https://example.org/collection"]) + + +def test_plan_execute_raise_without_injected_dependencies(): + compiler = BuildCompiler() + with pytest.raises(NotImplementedError, match="planning"): + compiler.plan({"x": 1}) + + with pytest.raises(NotImplementedError, match="execution"): + compiler.execute({"plan": 1}) + + +def test_plan_execute_full_build_delegate_to_injected_dependencies(): + planner = FakePlanner() + executor = FakeExecutor() + options = BuildOptions() + compiler = BuildCompiler(planner=planner, executor=executor, options=options) + + plan = compiler.plan("design") + result = compiler.execute(plan) + full = compiler.full_build("design") + + assert plan["plan"] == "design" + assert result["result"]["plan"] == "design" + assert full["result"]["plan"] == "design" + assert planner.calls + assert executor.calls + + +def test_module_level_full_build_uses_lightweight_constructor_path(): + planner = FakePlanner() + executor = FakeExecutor() + + result = full_build("design", planner=planner, executor=executor) + + assert result["result"]["plan"] == "design" + + +def test_module_level_full_build_uses_synbiohub_factory_path_when_requested(): + planner = FakePlanner() + executor = FakeExecutor() + + result = full_build( + "design", + planner=planner, + executor=executor, + collections=[], + sbh_registry=None, + auth_token=None, + sbol_doc=None, + ) + + assert result["result"]["plan"] == "design" From 2c672344bd84e3046e2badd4ede3d28c3fb22134 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 15:25:37 -0600 Subject: [PATCH 43/93] Implement SBOL resolver and inventory indexes foundation --- src/buildcompiler/inventory/__init__.py | 6 +- src/buildcompiler/inventory/inventory.py | 171 ++++++++++++++++++ src/buildcompiler/sbol/__init__.py | 6 +- src/buildcompiler/sbol/resolver.py | 70 +++++++ .../unit/inventory/test_inventory_indexes.py | 70 +++++++ tests/unit/sbol/test_resolver.py | 83 +++++++++ 6 files changed, 404 insertions(+), 2 deletions(-) create mode 100644 src/buildcompiler/inventory/inventory.py create mode 100644 src/buildcompiler/sbol/resolver.py create mode 100644 tests/unit/inventory/test_inventory_indexes.py create mode 100644 tests/unit/sbol/test_resolver.py diff --git a/src/buildcompiler/inventory/__init__.py b/src/buildcompiler/inventory/__init__.py index 5e19692..9558475 100644 --- a/src/buildcompiler/inventory/__init__.py +++ b/src/buildcompiler/inventory/__init__.py @@ -1 +1,5 @@ -"""Package scaffolding for clean architecture.""" +"""Inventory package exports for deterministic lookup/indexing contracts.""" + +from .inventory import Inventory + +__all__ = ["Inventory"] diff --git a/src/buildcompiler/inventory/inventory.py b/src/buildcompiler/inventory/inventory.py new file mode 100644 index 0000000..9196829 --- /dev/null +++ b/src/buildcompiler/inventory/inventory.py @@ -0,0 +1,171 @@ +"""Normalized inventory facade with eager deterministic indexes.""" + +from __future__ import annotations + +from collections import defaultdict + +from buildcompiler.domain import ( + BuildStage, + IndexedBackbone, + IndexedPlasmid, + IndexedReagent, + MaterialState, +) + + +_MATERIAL_ORDER = { + MaterialState.PLANNED: 0, + MaterialState.GENERATED: 1, + MaterialState.ASSEMBLED: 2, + MaterialState.TRANSFORMED: 3, + MaterialState.PLATED: 4, +} + + +class Inventory: + def __init__( + self, + *, + plasmids: list[IndexedPlasmid] | None = None, + backbones: list[IndexedBackbone] | None = None, + reagents: list[IndexedReagent] | None = None, + ) -> None: + self.plasmids_by_identity: dict[str, IndexedPlasmid] = {} + self.plasmids_by_insert_identity: dict[str, list[IndexedPlasmid]] = defaultdict(list) + self.plasmids_by_fusion_sites: dict[tuple[str, ...], list[IndexedPlasmid]] = defaultdict(list) + self.plasmids_by_antibiotic: dict[str, list[IndexedPlasmid]] = defaultdict(list) + + self.backbones_by_identity: dict[str, IndexedBackbone] = {} + self.backbones_by_fusion_sites_and_antibiotic: dict[ + tuple[tuple[str, ...], str], list[IndexedBackbone] + ] = defaultdict(list) + + self.reagents_by_identity: dict[str, IndexedReagent] = {} + self.reagents_by_name: dict[str, IndexedReagent] = {} + + self.generated_products_by_identity: dict[str, IndexedPlasmid] = {} + + for plasmid in plasmids or []: + self._add_plasmid(plasmid) + for backbone in backbones or []: + self._add_backbone(backbone) + for reagent in reagents or []: + self._add_reagent(reagent) + + def _sorted_plasmids(self, items: list[IndexedPlasmid]) -> list[IndexedPlasmid]: + return sorted(items, key=lambda p: p.identity) + + def _backbone_stage(self, backbone: IndexedBackbone) -> BuildStage | None: + raw = backbone.metadata.get("stage") if backbone.metadata else None + if raw is None: + return None + if isinstance(raw, BuildStage): + return raw + try: + return BuildStage(raw) + except ValueError: + return None + + def _add_plasmid(self, plasmid: IndexedPlasmid) -> None: + self.plasmids_by_identity[plasmid.identity] = plasmid + for insert_identity in sorted(plasmid.metadata.get("insert_identities", [])): + self.plasmids_by_insert_identity[insert_identity].append(plasmid) + self.plasmids_by_insert_identity[insert_identity] = self._sorted_plasmids( + self.plasmids_by_insert_identity[insert_identity] + ) + + fusion_sites = tuple(plasmid.metadata.get("fusion_sites", ())) + if fusion_sites: + self.plasmids_by_fusion_sites[fusion_sites].append(plasmid) + self.plasmids_by_fusion_sites[fusion_sites] = self._sorted_plasmids( + self.plasmids_by_fusion_sites[fusion_sites] + ) + + antibiotic = plasmid.metadata.get("antibiotic") + if antibiotic: + self.plasmids_by_antibiotic[antibiotic].append(plasmid) + self.plasmids_by_antibiotic[antibiotic] = self._sorted_plasmids( + self.plasmids_by_antibiotic[antibiotic] + ) + + def _add_backbone(self, backbone: IndexedBackbone) -> None: + self.backbones_by_identity[backbone.identity] = backbone + fusion_sites = tuple(backbone.metadata.get("fusion_sites", ())) + antibiotic = backbone.metadata.get("antibiotic") + if fusion_sites and antibiotic: + key = (fusion_sites, antibiotic) + self.backbones_by_fusion_sites_and_antibiotic[key].append(backbone) + self.backbones_by_fusion_sites_and_antibiotic[key] = sorted( + self.backbones_by_fusion_sites_and_antibiotic[key], + key=lambda b: b.identity, + ) + + def _add_reagent(self, reagent: IndexedReagent) -> None: + self.reagents_by_identity[reagent.identity] = reagent + if reagent.name: + self.reagents_by_name[reagent.name] = reagent + + def find_single_part_plasmids( + self, part_identity: str, *, antibiotic: str | None = None + ) -> list[IndexedPlasmid]: + matches = list(self.plasmids_by_insert_identity.get(part_identity, [])) + if antibiotic is not None: + matches = [p for p in matches if p.metadata.get("antibiotic") == antibiotic] + return self._sorted_plasmids(matches) + + def find_lvl1_region_plasmids( + self, + region_identity: str, + *, + min_material_state: MaterialState = MaterialState.PLANNED, + ) -> list[IndexedPlasmid]: + matches = self.plasmids_by_insert_identity.get(region_identity, []) + min_rank = _MATERIAL_ORDER[min_material_state] + filtered = [p for p in matches if _MATERIAL_ORDER[p.state] >= min_rank] + return self._sorted_plasmids(filtered) + + def find_backbone( + self, + *, + fusion_sites: tuple[str, ...] | None = None, + antibiotic: str | None = None, + stage: BuildStage | None = None, + ) -> IndexedBackbone | None: + if fusion_sites is not None and antibiotic is not None: + candidates = list( + self.backbones_by_fusion_sites_and_antibiotic.get( + (tuple(fusion_sites), antibiotic), [] + ) + ) + else: + candidates = sorted(self.backbones_by_identity.values(), key=lambda b: b.identity) + if fusion_sites is not None: + candidates = [ + b for b in candidates if tuple(b.metadata.get("fusion_sites", ())) == tuple(fusion_sites) + ] + if antibiotic is not None: + candidates = [b for b in candidates if b.metadata.get("antibiotic") == antibiotic] + if stage is not None: + candidates = [b for b in candidates if self._backbone_stage(b) == stage] + return candidates[0] if candidates else None + + def find_restriction_enzyme(self, name: str) -> IndexedReagent | None: + reagent = self.reagents_by_name.get(name) + if reagent and reagent.reagent_type == "restriction_enzyme": + return reagent + return None + + def find_ligase(self, preferred: str | None = None) -> IndexedReagent | None: + if preferred: + reagent = self.reagents_by_name.get(preferred) + if reagent and reagent.reagent_type == "ligase": + return reagent + ligases = sorted( + (r for r in self.reagents_by_identity.values() if r.reagent_type == "ligase"), + key=lambda r: r.identity, + ) + return ligases[0] if ligases else None + + def add_generated_product(self, product: IndexedPlasmid) -> None: + self.generated_products_by_identity[product.identity] = product + self._add_plasmid(product) diff --git a/src/buildcompiler/sbol/__init__.py b/src/buildcompiler/sbol/__init__.py index 5e19692..45d9597 100644 --- a/src/buildcompiler/sbol/__init__.py +++ b/src/buildcompiler/sbol/__init__.py @@ -1 +1,5 @@ -"""Package scaffolding for clean architecture.""" +"""SBOL package exports for clean architecture contracts.""" + +from .resolver import PullPolicy, SbolResolver + +__all__ = ["PullPolicy", "SbolResolver"] diff --git a/src/buildcompiler/sbol/resolver.py b/src/buildcompiler/sbol/resolver.py new file mode 100644 index 0000000..ced3fa7 --- /dev/null +++ b/src/buildcompiler/sbol/resolver.py @@ -0,0 +1,70 @@ +"""SBOL document resolver with deterministic pull policy.""" + +from __future__ import annotations + +from enum import Enum +from typing import Any, Callable + +import sbol2 + + +class PullPolicy(str, Enum): + """Resolver behavior for remote pull attempts.""" + + NEVER = "never" + MISSING_ONLY = "missing_only" + ALWAYS_REFRESH = "always_refresh" + + +class SbolResolver: + """Resolve SBOL objects by identity from a local document with optional pull fallback.""" + + def __init__( + self, + document: sbol2.Document, + *, + pull_policy: PullPolicy = PullPolicy.MISSING_ONLY, + pull_client: Callable[[str], Any] | None = None, + ) -> None: + self.document = document + self.pull_policy = pull_policy + self.pull_client = pull_client + + def maybe_pull(self, identity: str) -> Any | None: + if self.pull_policy == PullPolicy.NEVER: + return None + if self.pull_client is None: + return None + return self.pull_client(identity) + + def _get(self, identity: str, expected_type: type) -> Any: + if self.pull_policy == PullPolicy.ALWAYS_REFRESH: + self.maybe_pull(identity) + + obj = self.document.find(identity) + if isinstance(obj, expected_type): + return obj + + if self.pull_policy == PullPolicy.MISSING_ONLY: + self.maybe_pull(identity) + obj = self.document.find(identity) + if isinstance(obj, expected_type): + return obj + + raise LookupError( + f"Could not resolve {expected_type.__name__} with identity '{identity}'" + ) + + def get_component(self, identity: str) -> sbol2.ComponentDefinition: + return self._get(identity, sbol2.ComponentDefinition) + + def get_module(self, identity: str) -> sbol2.ModuleDefinition: + return self._get(identity, sbol2.ModuleDefinition) + + def get_combinatorial_derivation( + self, identity: str + ) -> sbol2.CombinatorialDerivation: + return self._get(identity, sbol2.CombinatorialDerivation) + + def get_implementation(self, identity: str) -> sbol2.Implementation: + return self._get(identity, sbol2.Implementation) diff --git a/tests/unit/inventory/test_inventory_indexes.py b/tests/unit/inventory/test_inventory_indexes.py new file mode 100644 index 0000000..18719e2 --- /dev/null +++ b/tests/unit/inventory/test_inventory_indexes.py @@ -0,0 +1,70 @@ +from buildcompiler.domain import BuildStage, IndexedBackbone, IndexedPlasmid, IndexedReagent, MaterialState +from buildcompiler.inventory import Inventory + + +def _plasmid(identity: str, inserts: list[str], fusion_sites=("A", "B"), antibiotic="Ampicillin", state=MaterialState.PLANNED): + return IndexedPlasmid( + identity=identity, + display_id=identity.rsplit("/", 1)[-1], + state=state, + metadata={ + "insert_identities": inserts, + "fusion_sites": fusion_sites, + "antibiotic": antibiotic, + }, + ) + + +def test_inventory_indexes_and_queries_are_deterministic(): + p2 = _plasmid("https://example.org/p2", ["https://example.org/partA"], state=MaterialState.GENERATED) + p1 = _plasmid("https://example.org/p1", ["https://example.org/partA", "https://example.org/region1"]) + + b1 = IndexedBackbone( + identity="https://example.org/b1", + metadata={"fusion_sites": ("A", "B"), "antibiotic": "Ampicillin", "stage": BuildStage.ASSEMBLY_LVL1.value}, + ) + b2 = IndexedBackbone( + identity="https://example.org/b2", + metadata={"fusion_sites": ("A", "B"), "antibiotic": "Ampicillin", "stage": BuildStage.ASSEMBLY_LVL2.value}, + ) + + e1 = IndexedReagent(identity="https://example.org/r1", name="BsaI", reagent_type="restriction_enzyme") + l1 = IndexedReagent(identity="https://example.org/r2", name="T4_DNA_ligase", reagent_type="ligase") + + inv = Inventory(plasmids=[p2, p1], backbones=[b2, b1], reagents=[e1, l1]) + + assert inv.plasmids_by_identity[p1.identity] == p1 + assert [p.identity for p in inv.plasmids_by_insert_identity["https://example.org/partA"]] == [p1.identity, p2.identity] + assert [p.identity for p in inv.plasmids_by_fusion_sites[("A", "B")]] == [p1.identity, p2.identity] + assert [p.identity for p in inv.plasmids_by_antibiotic["Ampicillin"]] == [p1.identity, p2.identity] + + key = (("A", "B"), "Ampicillin") + assert [b.identity for b in inv.backbones_by_fusion_sites_and_antibiotic[key]] == [b1.identity, b2.identity] + assert inv.find_backbone(fusion_sites=("A", "B"), antibiotic="Ampicillin", stage=BuildStage.ASSEMBLY_LVL1) == b1 + + assert inv.find_restriction_enzyme("BsaI") == e1 + assert inv.find_ligase("T4_DNA_ligase") == l1 + assert inv.find_ligase().identity == l1.identity + + assert [p.identity for p in inv.find_single_part_plasmids("https://example.org/partA")] == [p1.identity, p2.identity] + assert [p.identity for p in inv.find_lvl1_region_plasmids("https://example.org/region1")] == [p1.identity] + assert inv.find_lvl1_region_plasmids("https://example.org/partA", min_material_state=MaterialState.GENERATED) == [p2] + + +def test_add_generated_product_updates_indexes_immediately(): + inv = Inventory() + product = _plasmid( + "https://example.org/generated1", + ["https://example.org/partG"], + fusion_sites=("C", "D"), + antibiotic="Kanamycin", + state=MaterialState.GENERATED, + ) + + inv.add_generated_product(product) + + assert inv.generated_products_by_identity[product.identity] == product + assert inv.plasmids_by_identity[product.identity] == product + assert inv.find_single_part_plasmids("https://example.org/partG") == [product] + assert inv.plasmids_by_fusion_sites[("C", "D")] == [product] + assert inv.plasmids_by_antibiotic["Kanamycin"] == [product] diff --git a/tests/unit/sbol/test_resolver.py b/tests/unit/sbol/test_resolver.py new file mode 100644 index 0000000..a1361de --- /dev/null +++ b/tests/unit/sbol/test_resolver.py @@ -0,0 +1,83 @@ +import sbol2 +import pytest + +from buildcompiler.sbol import PullPolicy, SbolResolver + + +class FakePullClient: + def __init__(self, document: sbol2.Document) -> None: + self.document = document + self.calls: list[str] = [] + + def __call__(self, identity: str): + self.calls.append(identity) + return self.document.find(identity) + + +def _make_doc() -> tuple[sbol2.Document, dict[str, str]]: + doc = sbol2.Document() + ns = "https://example.org" + sbol2.setHomespace(ns) + + comp = sbol2.ComponentDefinition(f"{ns}/component") + mod = sbol2.ModuleDefinition(f"{ns}/module") + impl = sbol2.Implementation(f"{ns}/impl") + comb = sbol2.CombinatorialDerivation(f"{ns}/comb", comp.identity) + + doc.add(comp) + doc.add(mod) + doc.add(impl) + doc.add(comb) + return doc, { + "component": comp.identity, + "module": mod.identity, + "implementation": impl.identity, + "combinatorial": comb.identity, + } + + +def test_resolver_gets_expected_types_from_local_document(): + doc, ids = _make_doc() + resolver = SbolResolver(doc, pull_policy=PullPolicy.NEVER) + + assert resolver.get_component(ids["component"]).identity == ids["component"] + assert resolver.get_module(ids["module"]).identity == ids["module"] + assert resolver.get_implementation(ids["implementation"]).identity == ids["implementation"] + assert ( + resolver.get_combinatorial_derivation(ids["combinatorial"]).identity + == ids["combinatorial"] + ) + + +def test_never_policy_does_not_pull_on_miss(): + doc, _ = _make_doc() + fake = FakePullClient(doc) + resolver = SbolResolver(doc, pull_policy=PullPolicy.NEVER, pull_client=fake) + + with pytest.raises(LookupError): + resolver.get_component("https://example.org/missing") + + assert fake.calls == [] + + +def test_missing_only_pulls_only_when_local_lookup_misses(): + doc, ids = _make_doc() + fake = FakePullClient(doc) + resolver = SbolResolver(doc, pull_policy=PullPolicy.MISSING_ONLY, pull_client=fake) + + resolver.get_component(ids["component"]) + assert fake.calls == [] + + with pytest.raises(LookupError): + resolver.get_component("https://example.org/missing") + assert fake.calls == ["https://example.org/missing"] + + +def test_always_refresh_pulls_even_on_hit(): + doc, ids = _make_doc() + fake = FakePullClient(doc) + resolver = SbolResolver(doc, pull_policy=PullPolicy.ALWAYS_REFRESH, pull_client=fake) + + resolver.get_component(ids["component"]) + + assert fake.calls == [ids["component"]] From c621e457a721a3f34adef30d47abdcd2317cec7f Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 15:42:21 -0600 Subject: [PATCH 44/93] Fix generated product reindexing for existing plasmids --- src/buildcompiler/inventory/inventory.py | 31 +++++++++++++++++++ .../unit/inventory/test_inventory_indexes.py | 30 ++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/buildcompiler/inventory/inventory.py b/src/buildcompiler/inventory/inventory.py index 9196829..fb06999 100644 --- a/src/buildcompiler/inventory/inventory.py +++ b/src/buildcompiler/inventory/inventory.py @@ -66,7 +66,38 @@ def _backbone_stage(self, backbone: IndexedBackbone) -> BuildStage | None: except ValueError: return None + def _remove_plasmid_from_secondary_indexes(self, plasmid: IndexedPlasmid) -> None: + for insert_identity in sorted(plasmid.metadata.get("insert_identities", [])): + existing = self.plasmids_by_insert_identity.get(insert_identity, []) + filtered = [indexed for indexed in existing if indexed.identity != plasmid.identity] + if filtered: + self.plasmids_by_insert_identity[insert_identity] = filtered + else: + self.plasmids_by_insert_identity.pop(insert_identity, None) + + fusion_sites = tuple(plasmid.metadata.get("fusion_sites", ())) + if fusion_sites: + existing = self.plasmids_by_fusion_sites.get(fusion_sites, []) + filtered = [indexed for indexed in existing if indexed.identity != plasmid.identity] + if filtered: + self.plasmids_by_fusion_sites[fusion_sites] = filtered + else: + self.plasmids_by_fusion_sites.pop(fusion_sites, None) + + antibiotic = plasmid.metadata.get("antibiotic") + if antibiotic: + existing = self.plasmids_by_antibiotic.get(antibiotic, []) + filtered = [indexed for indexed in existing if indexed.identity != plasmid.identity] + if filtered: + self.plasmids_by_antibiotic[antibiotic] = filtered + else: + self.plasmids_by_antibiotic.pop(antibiotic, None) + def _add_plasmid(self, plasmid: IndexedPlasmid) -> None: + existing = self.plasmids_by_identity.get(plasmid.identity) + if existing is not None: + self._remove_plasmid_from_secondary_indexes(existing) + self.plasmids_by_identity[plasmid.identity] = plasmid for insert_identity in sorted(plasmid.metadata.get("insert_identities", [])): self.plasmids_by_insert_identity[insert_identity].append(plasmid) diff --git a/tests/unit/inventory/test_inventory_indexes.py b/tests/unit/inventory/test_inventory_indexes.py index 18719e2..2ab1796 100644 --- a/tests/unit/inventory/test_inventory_indexes.py +++ b/tests/unit/inventory/test_inventory_indexes.py @@ -68,3 +68,33 @@ def test_add_generated_product_updates_indexes_immediately(): assert inv.find_single_part_plasmids("https://example.org/partG") == [product] assert inv.plasmids_by_fusion_sites[("C", "D")] == [product] assert inv.plasmids_by_antibiotic["Kanamycin"] == [product] + + +def test_add_generated_product_replaces_existing_secondary_indexes(): + inv = Inventory() + original = _plasmid( + "https://example.org/generated2", + ["https://example.org/partOld"], + fusion_sites=("A", "B"), + antibiotic="Ampicillin", + state=MaterialState.GENERATED, + ) + updated = _plasmid( + "https://example.org/generated2", + ["https://example.org/partNew"], + fusion_sites=("C", "D"), + antibiotic="Kanamycin", + state=MaterialState.ASSEMBLED, + ) + + inv.add_generated_product(original) + inv.add_generated_product(updated) + + assert inv.plasmids_by_identity[updated.identity] == updated + assert inv.generated_products_by_identity[updated.identity] == updated + assert inv.find_single_part_plasmids("https://example.org/partOld") == [] + assert inv.find_single_part_plasmids("https://example.org/partNew") == [updated] + assert inv.plasmids_by_fusion_sites.get(("A", "B"), []) == [] + assert inv.plasmids_by_fusion_sites[("C", "D")] == [updated] + assert inv.plasmids_by_antibiotic.get("Ampicillin", []) == [] + assert inv.plasmids_by_antibiotic["Kanamycin"] == [updated] From adf812cbed5865c37c483334432b75d72c98195a Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 15:48:49 -0600 Subject: [PATCH 45/93] Fix resolver to accept pulled SBOL objects directly --- src/buildcompiler/sbol/resolver.py | 8 ++++++-- tests/unit/sbol/test_resolver.py | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/buildcompiler/sbol/resolver.py b/src/buildcompiler/sbol/resolver.py index ced3fa7..3618983 100644 --- a/src/buildcompiler/sbol/resolver.py +++ b/src/buildcompiler/sbol/resolver.py @@ -39,14 +39,18 @@ def maybe_pull(self, identity: str) -> Any | None: def _get(self, identity: str, expected_type: type) -> Any: if self.pull_policy == PullPolicy.ALWAYS_REFRESH: - self.maybe_pull(identity) + pulled = self.maybe_pull(identity) + if isinstance(pulled, expected_type): + return pulled obj = self.document.find(identity) if isinstance(obj, expected_type): return obj if self.pull_policy == PullPolicy.MISSING_ONLY: - self.maybe_pull(identity) + pulled = self.maybe_pull(identity) + if isinstance(pulled, expected_type): + return pulled obj = self.document.find(identity) if isinstance(obj, expected_type): return obj diff --git a/tests/unit/sbol/test_resolver.py b/tests/unit/sbol/test_resolver.py index a1361de..f3b4297 100644 --- a/tests/unit/sbol/test_resolver.py +++ b/tests/unit/sbol/test_resolver.py @@ -14,6 +14,16 @@ def __call__(self, identity: str): return self.document.find(identity) +class ReturnOnlyPullClient: + def __init__(self, pulled_objects: dict[str, object]) -> None: + self.pulled_objects = pulled_objects + self.calls: list[str] = [] + + def __call__(self, identity: str): + self.calls.append(identity) + return self.pulled_objects.get(identity) + + def _make_doc() -> tuple[sbol2.Document, dict[str, str]]: doc = sbol2.Document() ns = "https://example.org" @@ -81,3 +91,19 @@ def test_always_refresh_pulls_even_on_hit(): resolver.get_component(ids["component"]) assert fake.calls == [ids["component"]] + + +def test_missing_only_returns_object_from_pull_client_without_document_mutation(): + local_doc = sbol2.Document() + ns = "https://example.org" + sbol2.setHomespace(ns) + remote_component = sbol2.ComponentDefinition(f"{ns}/remote-component") + pull_client = ReturnOnlyPullClient({remote_component.identity: remote_component}) + resolver = SbolResolver( + local_doc, pull_policy=PullPolicy.MISSING_ONLY, pull_client=pull_client + ) + + resolved = resolver.get_component(remote_component.identity) + + assert resolved is remote_component + assert pull_client.calls == [remote_component.identity] From 3196051a9a4c5b401394395067a8d61f88796d57 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 19:58:46 -0600 Subject: [PATCH 46/93] Implement full-build planning classifier and combinatorial expansion --- src/buildcompiler/planning/__init__.py | 7 +- src/buildcompiler/planning/classifier.py | 79 ++++++++++ src/buildcompiler/planning/combinatorial.py | 110 ++++++++++++++ .../planning/full_build_planner.py | 49 ++++++ src/buildcompiler/planning/models.py | 24 +++ src/buildcompiler/planning/validation.py | 140 ++++++++++++++++++ tests/unit/planning/test_classifier.py | 35 +++++ tests/unit/planning/test_combinatorial.py | 48 ++++++ .../unit/planning/test_full_build_planner.py | 23 +++ tests/unit/planning/test_validation.py | 49 ++++++ 10 files changed, 563 insertions(+), 1 deletion(-) create mode 100644 src/buildcompiler/planning/classifier.py create mode 100644 src/buildcompiler/planning/combinatorial.py create mode 100644 src/buildcompiler/planning/full_build_planner.py create mode 100644 src/buildcompiler/planning/models.py create mode 100644 src/buildcompiler/planning/validation.py create mode 100644 tests/unit/planning/test_classifier.py create mode 100644 tests/unit/planning/test_combinatorial.py create mode 100644 tests/unit/planning/test_full_build_planner.py create mode 100644 tests/unit/planning/test_validation.py diff --git a/src/buildcompiler/planning/__init__.py b/src/buildcompiler/planning/__init__.py index 5e19692..5dd1576 100644 --- a/src/buildcompiler/planning/__init__.py +++ b/src/buildcompiler/planning/__init__.py @@ -1 +1,6 @@ -"""Package scaffolding for clean architecture.""" +"""Planning package exports.""" + +from .full_build_planner import FullBuildPlanner +from .models import BuildPlan, UnsupportedPlanningRecord + +__all__ = ["BuildPlan", "UnsupportedPlanningRecord", "FullBuildPlanner"] diff --git a/src/buildcompiler/planning/classifier.py b/src/buildcompiler/planning/classifier.py new file mode 100644 index 0000000..462e493 --- /dev/null +++ b/src/buildcompiler/planning/classifier.py @@ -0,0 +1,79 @@ +"""Design classification helpers for planning.""" + +from __future__ import annotations + +import re + +import sbol2 + +from buildcompiler.domain import BuildRequest, BuildStage, DesignKind +from buildcompiler.planning.models import UnsupportedPlanningRecord +from buildcompiler.planning.validation import classify_part_role + + +def _stable_slug(identity: str) -> str: + return re.sub(r"[^a-z0-9]+", "-", identity.lower()).strip("-")[-48:] + + +def request_id_for( + stage: BuildStage, + source_identity: str, + source_display_id: str | None, + *, + variant_index: int | None = None, +) -> str: + base = source_display_id or _stable_slug(source_identity) + rid = f"{stage.value}:{base}" + if variant_index is not None: + rid = f"{rid}:v{variant_index}" + return rid + + +def classify_non_combinatorial( + design: object, +) -> BuildRequest | UnsupportedPlanningRecord: + if isinstance(design, sbol2.ModuleDefinition): + return BuildRequest( + request_id_for(BuildStage.ASSEMBLY_LVL2, design.identity, design.displayId), + BuildStage.ASSEMBLY_LVL2, + design.identity, + design.displayId, + DesignKind.MODULE_DEFINITION, + ) + + if isinstance(design, sbol2.ComponentDefinition): + count = len(design.components) + if count > 1: + return BuildRequest( + request_id_for( + BuildStage.ASSEMBLY_LVL1, design.identity, design.displayId + ), + BuildStage.ASSEMBLY_LVL1, + design.identity, + design.displayId, + DesignKind.COMPONENT_DEFINITION, + ) + if count <= 1 and classify_part_role(design) is not None: + return BuildRequest( + request_id_for( + BuildStage.DOMESTICATION, design.identity, design.displayId + ), + BuildStage.DOMESTICATION, + design.identity, + design.displayId, + DesignKind.COMPONENT_DEFINITION, + ) + return UnsupportedPlanningRecord( + design.identity, + design.displayId, + DesignKind.COMPONENT_DEFINITION, + "ComponentDefinition is single/empty with unsupported role.", + {"component_count": count, "roles": list(design.roles)}, + ) + + return UnsupportedPlanningRecord( + getattr(design, "identity", str(design)), + getattr(design, "displayId", None), + DesignKind.UNSUPPORTED, + f"Unsupported design type: {type(design).__name__}", + ) diff --git a/src/buildcompiler/planning/combinatorial.py b/src/buildcompiler/planning/combinatorial.py new file mode 100644 index 0000000..49d98af --- /dev/null +++ b/src/buildcompiler/planning/combinatorial.py @@ -0,0 +1,110 @@ +from __future__ import annotations +import itertools +import sbol2 +from buildcompiler.api import BuildOptions +from buildcompiler.domain import BuildRequest, BuildStage, BuildWarning, DesignKind +from buildcompiler.planning.classifier import request_id_for +from buildcompiler.planning.models import UnsupportedPlanningRecord +from buildcompiler.planning.validation import ROLE_TO_NAME + + +def _collect_variant_sets(derivation): + variables = sorted(list(derivation.variableComponents), key=lambda v: v.identity) + return variables, [sorted(list(vc.variants), key=str) for vc in variables] + + +def expand_combinatorial_derivation( + derivation: sbol2.CombinatorialDerivation, *, options: BuildOptions +): + warnings = [] + unsupported = [] + requests = [] + variables, variant_sets = _collect_variant_sets(derivation) + if not variant_sets or any(len(v) == 0 for v in variant_sets): + unsupported.append( + UnsupportedPlanningRecord( + derivation.identity, + derivation.displayId, + DesignKind.COMBINATORIAL_DERIVATION, + "Variable component has no listed variants.", + ) + ) + return requests, unsupported, warnings + total = 1 + for v in variant_sets: + total *= len(v) + if ( + total > options.planning.combinatorial.max_variants + and not options.planning.combinatorial.allow_large_expansion + ): + warnings.append( + BuildWarning( + "planning.combinatorial.expansion_blocked", + "Combinatorial expansion exceeds max_variants and is blocked.", + BuildStage.ASSEMBLY_LVL1, + derivation.identity, + { + "variant_count": total, + "max_variants": options.planning.combinatorial.max_variants, + }, + ) + ) + unsupported.append( + UnsupportedPlanningRecord( + derivation.identity, + derivation.displayId, + DesignKind.COMBINATORIAL_DERIVATION, + "Combinatorial expansion blocked by max_variants.", + {"variant_count": total}, + ) + ) + return requests, unsupported, warnings + doc = derivation.doc + valid = 0 + for idx, chosen in enumerate(itertools.product(*variant_sets)): + roles = [] + for pid in chosen: + obj = doc.find(pid) if doc is not None else None + if isinstance(obj, sbol2.ComponentDefinition): + matching = [ROLE_TO_NAME[r] for r in obj.roles if r in ROLE_TO_NAME] + if len(matching) == 1: + roles.append(matching[0]) + if sorted(roles) != ["cds", "promoter", "rbs", "terminator"]: + warnings.append( + BuildWarning( + "planning.combinatorial.invalid_variant", + "Skipping invalid combinatorial variant.", + BuildStage.ASSEMBLY_LVL1, + derivation.identity, + {"variant_index": idx}, + ) + ) + continue + requests.append( + BuildRequest( + request_id_for( + BuildStage.ASSEMBLY_LVL1, + derivation.identity, + derivation.displayId, + variant_index=idx, + ), + BuildStage.ASSEMBLY_LVL1, + derivation.identity, + derivation.displayId, + DesignKind.COMBINATORIAL_DERIVATION, + parent_group=derivation.identity, + variant_index=idx, + constraints={"part_order": list(chosen)}, + ) + ) + valid += 1 + if valid == 0: + unsupported.append( + UnsupportedPlanningRecord( + derivation.identity, + derivation.displayId, + DesignKind.COMBINATORIAL_DERIVATION, + "All combinatorial variants were invalid.", + ) + ) + return requests, unsupported, warnings diff --git a/src/buildcompiler/planning/full_build_planner.py b/src/buildcompiler/planning/full_build_planner.py new file mode 100644 index 0000000..a4189fa --- /dev/null +++ b/src/buildcompiler/planning/full_build_planner.py @@ -0,0 +1,49 @@ +"""Pure full-build planner orchestration.""" + +from __future__ import annotations + +from collections.abc import Iterable + +import sbol2 + +from buildcompiler.api import BuildOptions +from buildcompiler.planning.classifier import classify_non_combinatorial +from buildcompiler.planning.combinatorial import expand_combinatorial_derivation +from buildcompiler.planning.models import BuildPlan, UnsupportedPlanningRecord +from buildcompiler.sbol import SbolResolver + + +class FullBuildPlanner: + def __init__( + self, + *, + options: BuildOptions | None = None, + resolver: SbolResolver | None = None, + ): + self.options = options or BuildOptions() + self.resolver = resolver + + def plan( + self, abstract_designs: Iterable[object], *, options: BuildOptions | None = None + ) -> BuildPlan: + active = options or self.options + out = BuildPlan() + for design in abstract_designs: + if isinstance(design, sbol2.CombinatorialDerivation): + reqs, unsupported, warnings = expand_combinatorial_derivation( + design, options=active + ) + out.lvl1_requests.extend(reqs) + out.unsupported.extend(unsupported) + out.warnings.extend(warnings) + continue + classified = classify_non_combinatorial(design) + if isinstance(classified, UnsupportedPlanningRecord): + out.unsupported.append(classified) + elif classified.stage.value == "assembly_lvl2": + out.lvl2_requests.append(classified) + elif classified.stage.value == "assembly_lvl1": + out.lvl1_requests.append(classified) + else: + out.domestication_requests.append(classified) + return out diff --git a/src/buildcompiler/planning/models.py b/src/buildcompiler/planning/models.py new file mode 100644 index 0000000..d4782c7 --- /dev/null +++ b/src/buildcompiler/planning/models.py @@ -0,0 +1,24 @@ +"""Planning contracts for full-build planning output.""" + +from dataclasses import dataclass, field +from typing import Any + +from buildcompiler.domain import BuildRequest, BuildWarning, DesignKind + + +@dataclass +class UnsupportedPlanningRecord: + source_identity: str + source_display_id: str | None + source_kind: DesignKind + reason: str + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class BuildPlan: + lvl2_requests: list[BuildRequest] = field(default_factory=list) + lvl1_requests: list[BuildRequest] = field(default_factory=list) + domestication_requests: list[BuildRequest] = field(default_factory=list) + unsupported: list[UnsupportedPlanningRecord] = field(default_factory=list) + warnings: list[BuildWarning] = field(default_factory=list) diff --git a/src/buildcompiler/planning/validation.py b/src/buildcompiler/planning/validation.py new file mode 100644 index 0000000..b00c70e --- /dev/null +++ b/src/buildcompiler/planning/validation.py @@ -0,0 +1,140 @@ +"""Deterministic validation helpers for level-1 designs.""" + +from __future__ import annotations + +from collections import Counter + +import sbol2 + +from buildcompiler.constants import PART_ROLES +from buildcompiler.domain import BuildStage, BuildWarning + +ROLE_TO_NAME = { + "http://identifiers.org/so/SO:0000167": "promoter", + "http://identifiers.org/so/SO:0000139": "rbs", + "http://identifiers.org/so/SO:0000316": "cds", + "http://identifiers.org/so/SO:0000141": "terminator", +} +CANONICAL_ROLE_ORDER = ["promoter", "rbs", "cds", "terminator"] + + +def classify_part_role(component_definition: sbol2.ComponentDefinition) -> str | None: + matches = sorted( + ROLE_TO_NAME[role] for role in component_definition.roles if role in PART_ROLES + ) + if len(matches) != 1: + return None + return matches[0] + + +def validate_lvl1_cardinality( + component_definition: sbol2.ComponentDefinition, +) -> tuple[bool, list[BuildWarning]]: + warnings: list[BuildWarning] = [] + if len(component_definition.components) != 4: + warnings.append( + BuildWarning( + code="lvl1.variable_length", + message="Level-1 v1 requires exactly four components.", + stage=BuildStage.ASSEMBLY_LVL1, + source_identity=component_definition.identity, + ) + ) + return False, warnings + + roles = [] + for comp in component_definition.components: + target = ( + comp.doc.find(comp.definition) + if getattr(comp, "doc", None) is not None + else None + ) + if not isinstance(target, sbol2.ComponentDefinition): + continue + role = classify_part_role(target) + if role is not None: + roles.append(role) + + counts = Counter(roles) + ok = True + for required in CANONICAL_ROLE_ORDER: + if counts[required] != 1: + ok = False + warnings.append( + BuildWarning( + code="lvl1.invalid_cardinality", + message=f"Expected exactly one {required}, found {counts[required]}.", + stage=BuildStage.ASSEMBLY_LVL1, + source_identity=component_definition.identity, + metadata={"role": required, "count": counts[required]}, + ) + ) + return ok, warnings + + +def ordered_lvl1_parts( + component_definition: sbol2.ComponentDefinition, +) -> tuple[list[str], list[BuildWarning]]: + warnings: list[BuildWarning] = [] + role_to_identity: dict[str, str] = {} + comp_by_identity: dict[str, sbol2.Component] = {} + for comp in component_definition.components: + target = ( + comp.doc.find(comp.definition) + if getattr(comp, "doc", None) is not None + else None + ) + if isinstance(target, sbol2.ComponentDefinition): + role = classify_part_role(target) + if role: + role_to_identity[role] = target.identity + comp_by_identity[target.identity] = comp + + if len(role_to_identity) < 4: + return [ + role_to_identity[r] for r in CANONICAL_ROLE_ORDER if r in role_to_identity + ], warnings + + try: + ordered_components = list(component_definition.getInSequentialOrder()) + except Exception: + ordered_components = [] + + if len(ordered_components) == 4: + ordered_part_ids = [] + ordered_roles = [] + for comp in ordered_components: + target = ( + comp.doc.find(comp.definition) + if getattr(comp, "doc", None) is not None + else None + ) + if isinstance(target, sbol2.ComponentDefinition): + role = classify_part_role(target) + if role: + ordered_roles.append(role) + ordered_part_ids.append(target.identity) + if len(ordered_part_ids) == 4 and set(ordered_roles) == set( + CANONICAL_ROLE_ORDER + ): + if ordered_roles != CANONICAL_ROLE_ORDER: + warnings.append( + BuildWarning( + code="lvl1.non_canonical_order", + message="SBOL order is non-canonical and will be preserved.", + stage=BuildStage.ASSEMBLY_LVL1, + source_identity=component_definition.identity, + metadata={"ordered_roles": ordered_roles}, + ) + ) + return ordered_part_ids, warnings + + warnings.append( + BuildWarning( + code="lvl1.ambiguous_order", + message="SBOL order unavailable or ambiguous; using canonical order.", + stage=BuildStage.ASSEMBLY_LVL1, + source_identity=component_definition.identity, + ) + ) + return [role_to_identity[role] for role in CANONICAL_ROLE_ORDER], warnings diff --git a/tests/unit/planning/test_classifier.py b/tests/unit/planning/test_classifier.py new file mode 100644 index 0000000..47b2f8d --- /dev/null +++ b/tests/unit/planning/test_classifier.py @@ -0,0 +1,35 @@ +import sbol2 + +from buildcompiler.domain import BuildStage +from buildcompiler.planning.classifier import classify_non_combinatorial, request_id_for +from buildcompiler.planning.models import UnsupportedPlanningRecord + + +def test_classifier_maps_module_and_components(): + sbol2.setHomespace("https://example.org") + md = sbol2.ModuleDefinition("https://example.org/mod") + out = classify_non_combinatorial(md) + assert out.stage == BuildStage.ASSEMBLY_LVL2 + + er = sbol2.ComponentDefinition("https://example.org/er") + p = sbol2.ComponentDefinition("https://example.org/p") + p.roles = ["http://identifiers.org/so/SO:0000167"] + er.components.create("c1").definition = p.identity + er.components.create("c2").definition = p.identity + out2 = classify_non_combinatorial(er) + assert out2.stage == BuildStage.ASSEMBLY_LVL1 + + +def test_classifier_domestication_and_unsupported_and_deterministic_id(): + part = sbol2.ComponentDefinition("https://example.org/part") + part.roles = ["http://identifiers.org/so/SO:0000139"] + out = classify_non_combinatorial(part) + assert out.stage == BuildStage.DOMESTICATION + + unknown = sbol2.ComponentDefinition("https://example.org/u") + bad = classify_non_combinatorial(unknown) + assert isinstance(bad, UnsupportedPlanningRecord) + + rid1 = request_id_for(BuildStage.ASSEMBLY_LVL1, "https://example.org/A", "A") + rid2 = request_id_for(BuildStage.ASSEMBLY_LVL1, "https://example.org/A", "A") + assert rid1 == rid2 diff --git a/tests/unit/planning/test_combinatorial.py b/tests/unit/planning/test_combinatorial.py new file mode 100644 index 0000000..c4eb352 --- /dev/null +++ b/tests/unit/planning/test_combinatorial.py @@ -0,0 +1,48 @@ +import sbol2 +from buildcompiler.api import BuildOptions +from buildcompiler.planning.combinatorial import expand_combinatorial_derivation + +ROLE_URIS = [ + "http://identifiers.org/so/SO:0000167", + "http://identifiers.org/so/SO:0000139", + "http://identifiers.org/so/SO:0000316", + "http://identifiers.org/so/SO:0000141", +] + + +def _build_comb(valid=True): + sbol2.setHomespace("https://example.org") + doc = sbol2.Document() + template = sbol2.ComponentDefinition("https://example.org/template") + doc.add(template) + comb = sbol2.CombinatorialDerivation("https://example.org/comb", template.identity) + doc.add(comb) + roles = ROLE_URIS if valid else ROLE_URIS[:3] + for i, role in enumerate(roles): + vc = comb.variableComponents.create(f"https://example.org/var{i}") + p = sbol2.ComponentDefinition(f"https://example.org/v{i}") + p.roles = [role] + doc.add(p) + vc.variants = [p.identity] + return comb + + +def test_expansion_and_blocking_behaviors(): + comb = _build_comb(valid=True) + reqs, unsupported, warnings = expand_combinatorial_derivation( + comb, options=BuildOptions() + ) + assert len(reqs) == 1 and unsupported == [] and reqs[0].variant_index == 0 + limited = BuildOptions() + limited.planning.combinatorial.max_variants = 0 + reqs2, unsupported2, warnings2 = expand_combinatorial_derivation( + comb, options=limited + ) + assert reqs2 == [] and unsupported2 + assert any(w.code == "planning.combinatorial.expansion_blocked" for w in warnings2) + invalid = _build_comb(valid=False) + reqs3, unsupported3, warnings3 = expand_combinatorial_derivation( + invalid, options=BuildOptions() + ) + assert reqs3 == [] and unsupported3 + assert any(w.code == "planning.combinatorial.invalid_variant" for w in warnings3) diff --git a/tests/unit/planning/test_full_build_planner.py b/tests/unit/planning/test_full_build_planner.py new file mode 100644 index 0000000..f585312 --- /dev/null +++ b/tests/unit/planning/test_full_build_planner.py @@ -0,0 +1,23 @@ +import sbol2 + +from buildcompiler.planning import FullBuildPlanner + + +def test_planner_mixed_inputs_returns_queues_and_warnings(): + sbol2.setHomespace("https://example.org") + mod = sbol2.ModuleDefinition("https://example.org/mod") + multi = sbol2.ComponentDefinition("https://example.org/multi") + p = sbol2.ComponentDefinition("https://example.org/p") + p.roles = ["http://identifiers.org/so/SO:0000167"] + multi.components.create("c1").definition = p.identity + multi.components.create("c2").definition = p.identity + dom = sbol2.ComponentDefinition("https://example.org/dom") + dom.roles = ["http://identifiers.org/so/SO:0000139"] + + planner = FullBuildPlanner() + plan = planner.plan([mod, multi, dom, object()]) + + assert len(plan.lvl2_requests) == 1 + assert len(plan.lvl1_requests) == 1 + assert len(plan.domestication_requests) == 1 + assert len(plan.unsupported) == 1 diff --git a/tests/unit/planning/test_validation.py b/tests/unit/planning/test_validation.py new file mode 100644 index 0000000..32b53ef --- /dev/null +++ b/tests/unit/planning/test_validation.py @@ -0,0 +1,49 @@ +import sbol2 +from buildcompiler.planning.validation import ( + ordered_lvl1_parts, + validate_lvl1_cardinality, +) + +ROLE_URIS = [ + "http://identifiers.org/so/SO:0000167", + "http://identifiers.org/so/SO:0000139", + "http://identifiers.org/so/SO:0000316", + "http://identifiers.org/so/SO:0000141", +] + + +def _mk_part(i, role, doc): + p = sbol2.ComponentDefinition(f"https://example.org/p{i}") + p.roles = [role] + doc.add(p) + return p + + +def _mk_lvl1(parts, doc): + d = sbol2.ComponentDefinition("https://example.org/lvl1") + for i, p in enumerate(parts): + d.components.create(f"c{i}").definition = p.identity + doc.add(d) + return d + + +def test_validate_cardinality_and_order_fallback_and_warning(): + sbol2.setHomespace("https://example.org") + doc = sbol2.Document() + parts = [_mk_part(i, r, doc) for i, r in enumerate(ROLE_URIS)] + lvl1 = _mk_lvl1(parts, doc) + ok, warnings = validate_lvl1_cardinality(lvl1) + assert ok is True + assert warnings == [] + ordered, ow = ordered_lvl1_parts(lvl1) + assert len(ordered) == 4 + + +def test_validate_missing_or_duplicate_role_fails(): + sbol2.setHomespace("https://example.org") + doc = sbol2.Document() + p = _mk_part(0, ROLE_URIS[0], doc) + lvl1 = _mk_lvl1([p, p, p, p], doc) + ok, warnings = validate_lvl1_cardinality(lvl1) + assert ok is False + assert any(w.code == "lvl1.invalid_cardinality" for w in warnings) From d1e615f995b4b7c05a7d9caecd483eed81236186 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 20:23:06 -0600 Subject: [PATCH 47/93] Add lvl1 part-set validation in planning classifier --- src/buildcompiler/planning/classifier.py | 33 +++++++++++++++++++ tests/unit/planning/test_classifier.py | 22 ++++++++++++- .../unit/planning/test_full_build_planner.py | 10 +++++- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/buildcompiler/planning/classifier.py b/src/buildcompiler/planning/classifier.py index 462e493..5d666ae 100644 --- a/src/buildcompiler/planning/classifier.py +++ b/src/buildcompiler/planning/classifier.py @@ -3,6 +3,7 @@ from __future__ import annotations import re +from collections import Counter import sbol2 @@ -10,6 +11,8 @@ from buildcompiler.planning.models import UnsupportedPlanningRecord from buildcompiler.planning.validation import classify_part_role +RECOMMENDED_LVL1_PARTS = ("promoter", "rbs", "cds", "terminator") + def _stable_slug(identity: str) -> str: return re.sub(r"[^a-z0-9]+", "-", identity.lower()).strip("-")[-48:] @@ -44,6 +47,36 @@ def classify_non_combinatorial( if isinstance(design, sbol2.ComponentDefinition): count = len(design.components) if count > 1: + observed_roles: list[str] = [] + for component in design.components: + target = ( + component.doc.find(component.definition) + if getattr(component, "doc", None) is not None + else None + ) + if isinstance(target, sbol2.ComponentDefinition): + role = classify_part_role(target) + if role is not None: + observed_roles.append(role) + + counts = Counter(observed_roles) + missing = [role for role in RECOMMENDED_LVL1_PARTS if counts[role] != 1] + has_role_evidence = len(observed_roles) > 0 + if count != 4 or (has_role_evidence and missing): + return UnsupportedPlanningRecord( + design.identity, + design.displayId, + DesignKind.COMPONENT_DEFINITION, + "Warning: Level-1 planning expects exactly four parts (promoter, RBS, CDS, terminator).", + { + "component_count": count, + "observed_role_counts": { + role: counts.get(role, 0) + for role in RECOMMENDED_LVL1_PARTS + }, + "suggested_parts": list(RECOMMENDED_LVL1_PARTS), + }, + ) return BuildRequest( request_id_for( BuildStage.ASSEMBLY_LVL1, design.identity, design.displayId diff --git a/tests/unit/planning/test_classifier.py b/tests/unit/planning/test_classifier.py index 47b2f8d..961e915 100644 --- a/tests/unit/planning/test_classifier.py +++ b/tests/unit/planning/test_classifier.py @@ -14,12 +14,32 @@ def test_classifier_maps_module_and_components(): er = sbol2.ComponentDefinition("https://example.org/er") p = sbol2.ComponentDefinition("https://example.org/p") p.roles = ["http://identifiers.org/so/SO:0000167"] + r = sbol2.ComponentDefinition("https://example.org/r") + r.roles = ["http://identifiers.org/so/SO:0000139"] + c = sbol2.ComponentDefinition("https://example.org/c") + c.roles = ["http://identifiers.org/so/SO:0000316"] + t = sbol2.ComponentDefinition("https://example.org/t") + t.roles = ["http://identifiers.org/so/SO:0000141"] er.components.create("c1").definition = p.identity - er.components.create("c2").definition = p.identity + er.components.create("c2").definition = r.identity + er.components.create("c3").definition = c.identity + er.components.create("c4").definition = t.identity out2 = classify_non_combinatorial(er) assert out2.stage == BuildStage.ASSEMBLY_LVL1 +def test_classifier_warns_for_invalid_lvl1_part_mix(): + design = sbol2.ComponentDefinition("https://example.org/invalid") + p = sbol2.ComponentDefinition("https://example.org/p2") + p.roles = ["http://identifiers.org/so/SO:0000167"] + design.components.create("c1").definition = p.identity + design.components.create("c2").definition = p.identity + + out = classify_non_combinatorial(design) + assert isinstance(out, UnsupportedPlanningRecord) + assert "promoter" in out.reason.lower() + + def test_classifier_domestication_and_unsupported_and_deterministic_id(): part = sbol2.ComponentDefinition("https://example.org/part") part.roles = ["http://identifiers.org/so/SO:0000139"] diff --git a/tests/unit/planning/test_full_build_planner.py b/tests/unit/planning/test_full_build_planner.py index f585312..4fb66a5 100644 --- a/tests/unit/planning/test_full_build_planner.py +++ b/tests/unit/planning/test_full_build_planner.py @@ -9,8 +9,16 @@ def test_planner_mixed_inputs_returns_queues_and_warnings(): multi = sbol2.ComponentDefinition("https://example.org/multi") p = sbol2.ComponentDefinition("https://example.org/p") p.roles = ["http://identifiers.org/so/SO:0000167"] + r = sbol2.ComponentDefinition("https://example.org/r") + r.roles = ["http://identifiers.org/so/SO:0000139"] + c = sbol2.ComponentDefinition("https://example.org/c") + c.roles = ["http://identifiers.org/so/SO:0000316"] + t = sbol2.ComponentDefinition("https://example.org/t") + t.roles = ["http://identifiers.org/so/SO:0000141"] multi.components.create("c1").definition = p.identity - multi.components.create("c2").definition = p.identity + multi.components.create("c2").definition = r.identity + multi.components.create("c3").definition = c.identity + multi.components.create("c4").definition = t.identity dom = sbol2.ComponentDefinition("https://example.org/dom") dom.roles = ["http://identifiers.org/so/SO:0000139"] From 9027a70ecb35a8f95d463b9f0b447eb8064a6f53 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 20:31:04 -0600 Subject: [PATCH 48/93] Fix combinatorial part ordering by variable reference --- src/buildcompiler/planning/combinatorial.py | 5 ++- tests/unit/planning/test_combinatorial.py | 36 +++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/buildcompiler/planning/combinatorial.py b/src/buildcompiler/planning/combinatorial.py index 49d98af..a94a8bc 100644 --- a/src/buildcompiler/planning/combinatorial.py +++ b/src/buildcompiler/planning/combinatorial.py @@ -9,7 +9,10 @@ def _collect_variant_sets(derivation): - variables = sorted(list(derivation.variableComponents), key=lambda v: v.identity) + variables = list(derivation.variableComponents) + variables.sort( + key=lambda variable: (str(getattr(variable, "variable", "")), variable.identity) + ) return variables, [sorted(list(vc.variants), key=str) for vc in variables] diff --git a/tests/unit/planning/test_combinatorial.py b/tests/unit/planning/test_combinatorial.py index c4eb352..ea54215 100644 --- a/tests/unit/planning/test_combinatorial.py +++ b/tests/unit/planning/test_combinatorial.py @@ -46,3 +46,39 @@ def test_expansion_and_blocking_behaviors(): ) assert reqs3 == [] and unsupported3 assert any(w.code == "planning.combinatorial.invalid_variant" for w in warnings3) + + +def test_part_order_follows_template_sequence_not_variable_ids(): + sbol2.setHomespace("https://example.org") + doc = sbol2.Document() + template = sbol2.ComponentDefinition("https://example.org/template_seq") + doc.add(template) + template_components = [] + for idx in range(4): + component = template.components.create(f"t_component_{idx}") + component.definition = f"https://example.org/template_part_{idx}" + template_components.append(component) + comb = sbol2.CombinatorialDerivation( + "https://example.org/comb_seq", template.identity + ) + doc.add(comb) + variable_specs = [ + ("var_z", template_components[0], ROLE_URIS[0], "p0"), + ("var_y", template_components[1], ROLE_URIS[1], "p1"), + ("var_x", template_components[2], ROLE_URIS[2], "p2"), + ("var_w", template_components[3], ROLE_URIS[3], "p3"), + ] + expected_order = [] + for var_id, template_component, role, part_id in variable_specs: + vc = comb.variableComponents.create(var_id) + vc.variable = template_component.identity + part = sbol2.ComponentDefinition(f"https://example.org/{part_id}") + part.roles = [role] + doc.add(part) + expected_order.append(part.identity) + vc.variants = [part.identity] + reqs, unsupported, warnings = expand_combinatorial_derivation( + comb, options=BuildOptions() + ) + assert len(reqs) == 1 and unsupported == [] + assert reqs[0].constraints["part_order"] == expected_order From 52d4ec423167440b2f1fa213eea27fcc4dddb69f Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 20:47:04 -0600 Subject: [PATCH 49/93] Implement compatibility selector and deterministic route scoring --- src/buildcompiler/inventory/__init__.py | 11 +- src/buildcompiler/inventory/compatibility.py | 58 ++++++++ src/buildcompiler/inventory/selector.py | 148 +++++++++++++++++++ tests/unit/inventory/test_compatibility.py | 13 ++ tests/unit/inventory/test_selector.py | 60 ++++++++ 5 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 src/buildcompiler/inventory/compatibility.py create mode 100644 src/buildcompiler/inventory/selector.py create mode 100644 tests/unit/inventory/test_compatibility.py create mode 100644 tests/unit/inventory/test_selector.py diff --git a/src/buildcompiler/inventory/__init__.py b/src/buildcompiler/inventory/__init__.py index 9558475..403b6e6 100644 --- a/src/buildcompiler/inventory/__init__.py +++ b/src/buildcompiler/inventory/__init__.py @@ -1,5 +1,14 @@ """Inventory package exports for deterministic lookup/indexing contracts.""" +from .compatibility import Lvl1Route, Lvl2Route, RouteScore, RouteSelection from .inventory import Inventory +from .selector import CompatibilitySelector -__all__ = ["Inventory"] +__all__ = [ + "CompatibilitySelector", + "Inventory", + "Lvl1Route", + "Lvl2Route", + "RouteScore", + "RouteSelection", +] diff --git a/src/buildcompiler/inventory/compatibility.py b/src/buildcompiler/inventory/compatibility.py new file mode 100644 index 0000000..52ddcb8 --- /dev/null +++ b/src/buildcompiler/inventory/compatibility.py @@ -0,0 +1,58 @@ +"""Deterministic compatibility route models and score ordering.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from buildcompiler.domain import IndexedBackbone, IndexedPlasmid + + +@dataclass(frozen=True) +class RouteScore: + missing_required_products: int = 0 + missing_domestications: int = 0 + missing_lvl1_plasmids: int = 0 + generated_or_planned_materials: int = 0 + lower_material_state_penalty: int = 0 + constraint_violations: int = 0 + total_assemblies: int = 0 + identity_tiebreak: tuple[str, ...] = () + + def sort_key(self) -> tuple[int, int, int, int, int, int, int, tuple[str, ...]]: + """Lower sort key is a better route.""" + return ( + self.constraint_violations, + self.missing_required_products, + self.missing_domestications, + self.missing_lvl1_plasmids, + self.generated_or_planned_materials, + self.lower_material_state_penalty, + self.total_assemblies, + self.identity_tiebreak, + ) + + +@dataclass(frozen=True) +class Lvl1Route: + request_id: str + part_identities: tuple[str, ...] + selected_part_plasmids: tuple[IndexedPlasmid, ...] + missing_part_identities: tuple[str, ...] + backbone: IndexedBackbone | None + score: RouteScore + + +@dataclass(frozen=True) +class Lvl2Route: + request_id: str + region_order: tuple[str, ...] + selected_lvl1_plasmids: tuple[IndexedPlasmid, ...] + missing_region_identities: tuple[str, ...] + backbone: IndexedBackbone | None + score: RouteScore + + +@dataclass(frozen=True) +class RouteSelection: + selected: Lvl1Route | Lvl2Route | None + rejected: tuple[Lvl1Route | Lvl2Route, ...] = () diff --git a/src/buildcompiler/inventory/selector.py b/src/buildcompiler/inventory/selector.py new file mode 100644 index 0000000..70aae00 --- /dev/null +++ b/src/buildcompiler/inventory/selector.py @@ -0,0 +1,148 @@ +"""Deterministic compatibility selector for lvl1/lvl2 route selection.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from itertools import permutations +from typing import Any + +from buildcompiler.api import BuildOptions +from buildcompiler.domain import BuildStage, MaterialState +from buildcompiler.inventory.compatibility import Lvl1Route, Lvl2Route, RouteScore, RouteSelection +from buildcompiler.inventory.inventory import Inventory + + +_STATE_RANK = { + MaterialState.PLANNED: 0, + MaterialState.GENERATED: 1, + MaterialState.ASSEMBLED: 2, + MaterialState.TRANSFORMED: 3, + MaterialState.PLATED: 4, +} + + +class CompatibilitySelector: + def __init__(self, inventory: Inventory, *, options: BuildOptions | None = None) -> None: + self.inventory = inventory + self.options = options or BuildOptions() + + def _is_generated_or_planned(self, plasmid: Any) -> bool: + source = (plasmid.metadata or {}).get("source", "") + if source: + return source in {"generated", "planned"} + return plasmid.state in {MaterialState.PLANNED, MaterialState.GENERATED} + + def _constraint_filter(self, items: list[Any], constraints: Mapping[str, Any]) -> list[Any]: + allowed = set(constraints.get("allowed_identities", [])) + forbidden = set(constraints.get("forbidden_identities", [])) + antibiotic = constraints.get("antibiotic") + out = [] + for item in items: + if allowed and item.identity not in allowed: + continue + if item.identity in forbidden: + continue + if antibiotic and item.metadata.get("antibiotic") != antibiotic: + continue + out.append(item) + return out + + def _best_candidate(self, candidates: list[Any], constraints: Mapping[str, Any]) -> Any | None: + filtered = self._constraint_filter(candidates, constraints) + if not filtered: + return None + + prefer_existing = self.options.selection.prefer_existing_collection_material + prefer_state = self.options.selection.prefer_higher_material_state + + def _key(p: Any) -> tuple[int, int, str]: + generated_penalty = int(prefer_existing and self._is_generated_or_planned(p)) + state_penalty = -_STATE_RANK[p.state] if prefer_state else 0 + return (generated_penalty, state_penalty, p.identity) + + return sorted(filtered, key=_key)[0] + + def select_lvl1_route(self, *, request_id: str, part_identities: Sequence[str], constraints: Mapping[str, Any] | None = None) -> RouteSelection: + active_constraints = constraints or {} + selected = [] + missing = [] + for part_identity in part_identities: + candidates = self.inventory.find_single_part_plasmids(part_identity, antibiotic=active_constraints.get("antibiotic")) + choice = self._best_candidate(candidates, active_constraints) + if choice is None: + missing.append(part_identity) + else: + selected.append(choice) + + backbone = self.inventory.find_backbone( + fusion_sites=tuple(active_constraints["fusion_sites"]) if "fusion_sites" in active_constraints else None, + antibiotic=active_constraints.get("antibiotic"), + stage=BuildStage.ASSEMBLY_LVL1, + ) + score = RouteScore( + missing_required_products=len(missing), + missing_domestications=len(missing), + generated_or_planned_materials=sum(1 for p in selected if self._is_generated_or_planned(p)), + lower_material_state_penalty=sum((_STATE_RANK[MaterialState.PLATED] - _STATE_RANK[p.state]) for p in selected) + if self.options.selection.prefer_higher_material_state + else 0, + identity_tiebreak=tuple(sorted(p.identity for p in selected)) + tuple(missing), + ) + route = Lvl1Route(request_id, tuple(part_identities), tuple(selected), tuple(missing), backbone, score) + return RouteSelection(selected=route, rejected=()) + + def select_lvl2_route(self, *, request_id: str, region_identities: Sequence[str], constraints: Mapping[str, Any] | None = None) -> RouteSelection: + active_constraints = constraints or {} + max_regions = self.options.planning.lvl2_search.max_exhaustive_region_count + allow_large = self.options.planning.lvl2_search.allow_large_order_search + + if "region_order" in active_constraints: + orders = [tuple(active_constraints["region_order"])] + elif len(region_identities) > max_regions and not allow_large: + blocked = Lvl2Route( + request_id=request_id, + region_order=tuple(region_identities), + selected_lvl1_plasmids=(), + missing_region_identities=tuple(region_identities), + backbone=None, + score=RouteScore( + missing_required_products=len(region_identities), + missing_lvl1_plasmids=len(region_identities), + constraint_violations=1, + identity_tiebreak=tuple(region_identities), + ), + ) + return RouteSelection(selected=None, rejected=(blocked,)) + else: + orders = sorted(set(permutations(region_identities))) + + routes = [] + for order in orders: + selected = [] + missing = [] + for region in order: + candidates = self.inventory.find_lvl1_region_plasmids(region) + choice = self._best_candidate(candidates, active_constraints) + if choice is None: + missing.append(region) + else: + selected.append(choice) + score = RouteScore( + missing_required_products=len(missing), + missing_lvl1_plasmids=len(missing), + generated_or_planned_materials=sum(1 for p in selected if self._is_generated_or_planned(p)), + lower_material_state_penalty=sum((_STATE_RANK[MaterialState.PLATED] - _STATE_RANK[p.state]) for p in selected) + if self.options.selection.prefer_higher_material_state + else 0, + total_assemblies=int(bool(missing)), + identity_tiebreak=tuple(p.identity for p in selected) + tuple(missing), + ) + backbone = self.inventory.find_backbone( + fusion_sites=tuple(active_constraints["fusion_sites"]) if "fusion_sites" in active_constraints else None, + antibiotic=active_constraints.get("antibiotic"), + stage=BuildStage.ASSEMBLY_LVL2, + ) + routes.append(Lvl2Route(request_id, tuple(order), tuple(selected), tuple(missing), backbone, score)) + + ranked = sorted(routes, key=lambda r: r.score.sort_key()) + return RouteSelection(selected=ranked[0] if ranked else None, rejected=tuple(ranked[1:4])) diff --git a/tests/unit/inventory/test_compatibility.py b/tests/unit/inventory/test_compatibility.py new file mode 100644 index 0000000..aa97a3d --- /dev/null +++ b/tests/unit/inventory/test_compatibility.py @@ -0,0 +1,13 @@ +from buildcompiler.inventory import RouteScore + + +def test_route_score_prefers_fewer_missing_domestications(): + assert RouteScore(missing_domestications=0).sort_key() < RouteScore(missing_domestications=1).sort_key() + + +def test_route_score_prefers_fewer_missing_lvl1_plasmids(): + assert RouteScore(missing_lvl1_plasmids=0).sort_key() < RouteScore(missing_lvl1_plasmids=1).sort_key() + + +def test_route_score_uses_stable_identity_tiebreak(): + assert RouteScore(identity_tiebreak=("a",)).sort_key() < RouteScore(identity_tiebreak=("b",)).sort_key() diff --git a/tests/unit/inventory/test_selector.py b/tests/unit/inventory/test_selector.py new file mode 100644 index 0000000..b78b7c9 --- /dev/null +++ b/tests/unit/inventory/test_selector.py @@ -0,0 +1,60 @@ +from buildcompiler.api import BuildOptions +from buildcompiler.domain import IndexedPlasmid, MaterialState +from buildcompiler.inventory import CompatibilitySelector, Inventory + + +def _plasmid(identity: str, insert: str, *, state=MaterialState.PLANNED, source="collection") -> IndexedPlasmid: + return IndexedPlasmid( + identity=identity, + state=state, + metadata={"insert_identities": [insert], "source": source, "antibiotic": "Ampicillin"}, + ) + + +def test_lvl1_missing_parts_are_reported_not_raised(): + inv = Inventory(plasmids=[_plasmid("https://e/p1", "https://e/part1")]) + sel = CompatibilitySelector(inv) + route = sel.select_lvl1_route(request_id="r1", part_identities=["https://e/part1", "https://e/part2"]).selected + assert route is not None + assert route.missing_part_identities == ("https://e/part2",) + + +def test_lvl1_prefers_existing_material_in_tie(): + inv = Inventory( + plasmids=[ + _plasmid("https://e/a", "https://e/part", source="generated", state=MaterialState.GENERATED), + _plasmid("https://e/b", "https://e/part", source="collection", state=MaterialState.GENERATED), + ] + ) + sel = CompatibilitySelector(inv) + route = sel.select_lvl1_route(request_id="r1", part_identities=["https://e/part"]).selected + assert route.selected_part_plasmids[0].identity == "https://e/b" + + +def test_lvl1_hard_constraints_override_selection_preference(): + inv = Inventory(plasmids=[_plasmid("https://e/a", "https://e/part", source="generated")]) + opts = BuildOptions() + opts.selection.prefer_existing_collection_material = True + sel = CompatibilitySelector(inv, options=opts) + route = sel.select_lvl1_route( + request_id="r1", + part_identities=["https://e/part"], + constraints={"allowed_identities": ["https://e/a"]}, + ).selected + assert route.selected_part_plasmids[0].identity == "https://e/a" + + +def test_lvl2_large_order_search_not_silent_without_opt_in(): + inv = Inventory() + sel = CompatibilitySelector(inv) + out = sel.select_lvl2_route(request_id="r2", region_identities=["a", "b", "c", "d", "e"]) + assert out.selected is None + assert out.rejected + + +def test_lvl2_rejected_alternatives_capped_at_3(): + inv = Inventory(plasmids=[_plasmid(f"https://e/p{i}", f"https://e/r{i}") for i in range(4)]) + sel = CompatibilitySelector(inv) + out = sel.select_lvl2_route(request_id="r3", region_identities=["https://e/r0", "https://e/r1", "https://e/r2", "https://e/r3"]) + assert out.selected is not None + assert len(out.rejected) == 3 From 2765225d2c7625a6e20085266d21591ed345307d Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 21:38:27 -0600 Subject: [PATCH 50/93] Fix lvl2 region_order constraint validation --- src/buildcompiler/inventory/selector.py | 19 ++++++++++++++++++- tests/unit/inventory/test_selector.py | 13 +++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/buildcompiler/inventory/selector.py b/src/buildcompiler/inventory/selector.py index 70aae00..07ddd31 100644 --- a/src/buildcompiler/inventory/selector.py +++ b/src/buildcompiler/inventory/selector.py @@ -97,7 +97,24 @@ def select_lvl2_route(self, *, request_id: str, region_identities: Sequence[str] allow_large = self.options.planning.lvl2_search.allow_large_order_search if "region_order" in active_constraints: - orders = [tuple(active_constraints["region_order"])] + constrained_order = tuple(active_constraints["region_order"]) + requested_regions = tuple(region_identities) + if sorted(constrained_order) != sorted(requested_regions): + blocked = Lvl2Route( + request_id=request_id, + region_order=constrained_order, + selected_lvl1_plasmids=(), + missing_region_identities=requested_regions, + backbone=None, + score=RouteScore( + missing_required_products=len(requested_regions), + missing_lvl1_plasmids=len(requested_regions), + constraint_violations=1, + identity_tiebreak=requested_regions, + ), + ) + return RouteSelection(selected=None, rejected=(blocked,)) + orders = [constrained_order] elif len(region_identities) > max_regions and not allow_large: blocked = Lvl2Route( request_id=request_id, diff --git a/tests/unit/inventory/test_selector.py b/tests/unit/inventory/test_selector.py index b78b7c9..fe4c03c 100644 --- a/tests/unit/inventory/test_selector.py +++ b/tests/unit/inventory/test_selector.py @@ -58,3 +58,16 @@ def test_lvl2_rejected_alternatives_capped_at_3(): out = sel.select_lvl2_route(request_id="r3", region_identities=["https://e/r0", "https://e/r1", "https://e/r2", "https://e/r3"]) assert out.selected is not None assert len(out.rejected) == 3 + + +def test_lvl2_constrained_order_must_match_requested_regions(): + inv = Inventory(plasmids=[_plasmid("https://e/p0", "https://e/r0"), _plasmid("https://e/p1", "https://e/r1")]) + sel = CompatibilitySelector(inv) + out = sel.select_lvl2_route( + request_id="r4", + region_identities=["https://e/r0", "https://e/r1"], + constraints={"region_order": ["https://e/r0"]}, + ) + assert out.selected is None + assert out.rejected + assert out.rejected[0].missing_region_identities == ("https://e/r0", "https://e/r1") From bc95f5f676a91959a2ac077fc6bf7688747c49ed Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 22:07:40 -0600 Subject: [PATCH 51/93] Add SBOL AssemblyService wrapper and contracts --- src/buildcompiler/sbol/__init__.py | 9 +- src/buildcompiler/sbol/assembly.py | 185 +++++++++++++++++++++++ tests/unit/sbol/test_assembly_service.py | 81 ++++++++++ 3 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 src/buildcompiler/sbol/assembly.py create mode 100644 tests/unit/sbol/test_assembly_service.py diff --git a/src/buildcompiler/sbol/__init__.py b/src/buildcompiler/sbol/__init__.py index 45d9597..d9e6f2c 100644 --- a/src/buildcompiler/sbol/__init__.py +++ b/src/buildcompiler/sbol/__init__.py @@ -1,5 +1,12 @@ """SBOL package exports for clean architecture contracts.""" +from .assembly import AssemblyJob, AssemblySbolResult, AssemblyService from .resolver import PullPolicy, SbolResolver -__all__ = ["PullPolicy", "SbolResolver"] +__all__ = [ + "AssemblyJob", + "AssemblySbolResult", + "AssemblyService", + "PullPolicy", + "SbolResolver", +] diff --git a/src/buildcompiler/sbol/assembly.py b/src/buildcompiler/sbol/assembly.py new file mode 100644 index 0000000..fa877d9 --- /dev/null +++ b/src/buildcompiler/sbol/assembly.py @@ -0,0 +1,185 @@ +"""SBOL assembly service wrapping legacy Golden Gate behavior.""" + +from dataclasses import dataclass, field + +import sbol2 + +from buildcompiler.domain import ( + BuildStage, + IndexedBackbone, + IndexedPlasmid, + IndexedReagent, + MaterialState, +) +from buildcompiler.sbol2build import Assembly + + +@dataclass +class _LegacyPlasmidAdapter: + plasmid_definition: sbol2.ComponentDefinition + plasmid_implementations: list[sbol2.Implementation] + + +@dataclass +class AssemblyJob: + """Normalized assembly inputs plus source/target SBOL documents.""" + + stage: BuildStage + product_identity: str + product_display_id: str + part_plasmids: list[IndexedPlasmid] + backbone: IndexedBackbone + restriction_enzyme: IndexedReagent + ligase: IndexedReagent + source_document: sbol2.Document + target_document: sbol2.Document + include_extracted_parts: bool = False + + +@dataclass +class AssemblySbolResult: + """Assembly output contract: normalized products plus stage document.""" + + products: list[IndexedPlasmid] + stage_document: sbol2.Document + activity_identity: str + logs: list[str] = field(default_factory=list) + + +class AssemblyService: + """Service wrapper that preserves legacy assembly internals behind normalized contracts.""" + + def run(self, job: AssemblyJob) -> AssemblySbolResult: + if not job.part_plasmids: + raise ValueError("AssemblyJob.part_plasmids must contain at least one plasmid") + + legacy_parts = [ + self._record_to_legacy_plasmid(record, job.source_document, "part_plasmids") + for record in job.part_plasmids + ] + legacy_backbone = self._record_to_legacy_plasmid( + IndexedPlasmid( + identity=job.backbone.identity, + display_id=job.backbone.display_id, + name=job.backbone.name, + metadata=job.backbone.metadata, + sbol_component=job.backbone.sbol_component, + ), + job.source_document, + "backbone", + ) + + restriction_impl = self._implementation_from_record( + job.restriction_enzyme, job.source_document + ) + ligase_impl = self._implementation_from_record(job.ligase, job.source_document) + + composite_prefix = job.product_display_id or job.product_identity.split("/")[-1] + legacy_assembly = Assembly( + part_plasmids=legacy_parts, + backbone_plasmid=legacy_backbone, + restriction_enzyme=restriction_impl, + ligase=ligase_impl, + source_document=job.source_document, + final_document=job.target_document, + composite_prefix=composite_prefix, + ) + legacy_products, final_doc = legacy_assembly.run( + include_extracted_parts=job.include_extracted_parts + ) + + products = [ + self._indexed_product_from_legacy_product(plasmid, job) + for plasmid in legacy_products + ] + logs = [ + f"Assembled {len(products)} product(s) at stage {job.stage.value}.", + f"Assembly activity: {legacy_assembly.assembly_activity.identity}", + ] + + return AssemblySbolResult( + products=products, + stage_document=final_doc, + activity_identity=legacy_assembly.assembly_activity.identity, + logs=logs, + ) + + def _record_to_legacy_plasmid( + self, + record: IndexedPlasmid, + source_document: sbol2.Document, + field_name: str, + ) -> _LegacyPlasmidAdapter: + component = self._component_from_record(record, source_document, field_name) + implementation = self._implementation_from_plasmid_record(record, source_document) + return _LegacyPlasmidAdapter(component, [implementation]) + + def _component_from_record( + self, + record: IndexedPlasmid, + source_document: sbol2.Document, + field_name: str, + ) -> sbol2.ComponentDefinition: + component = record.sbol_component or source_document.find(record.identity) + if component is None: + raise ValueError( + f"Missing SBOL ComponentDefinition for {field_name} record {record.identity}" + ) + if not isinstance(component, sbol2.ComponentDefinition): + raise ValueError( + f"{field_name} record {record.identity} must resolve to sbol2.ComponentDefinition" + ) + return component + + def _implementation_from_plasmid_record( + self, record: IndexedPlasmid, source_document: sbol2.Document + ) -> sbol2.Implementation: + impl_identity = record.metadata.get("implementation_identity") + implementation = source_document.find(impl_identity) if impl_identity else None + + if implementation is None: + component = self._component_from_record(record, source_document, "plasmid") + matches = [ + impl + for impl in source_document.implementations + if isinstance(impl, sbol2.Implementation) and impl.built == component.identity + ] + implementation = matches[0] if matches else None + + if implementation is None: + raise ValueError( + f"Missing SBOL Implementation for plasmid {record.identity}; " + "set metadata['implementation_identity'] or include implementation in source_document" + ) + return implementation + + def _implementation_from_record( + self, record: IndexedReagent, source_document: sbol2.Document + ) -> sbol2.Implementation: + impl_identity = record.metadata.get("implementation_identity") or record.identity + implementation = source_document.find(impl_identity) + if not isinstance(implementation, sbol2.Implementation): + raise ValueError( + "Missing SBOL Implementation for reagent " + f"{record.identity}; expected metadata['implementation_identity'] or identity to resolve" + ) + return implementation + + def _indexed_product_from_legacy_product( + self, product, job: AssemblyJob + ) -> IndexedPlasmid: + component = product.plasmid_definition + return IndexedPlasmid( + identity=component.identity, + display_id=component.displayId, + name=component.name, + state=MaterialState.GENERATED, + roles=list(component.roles), + metadata={ + "source_stage": job.stage.value, + "source_product_identity": job.product_identity, + "source_product_display_id": job.product_display_id, + "assembly_activity_identity": product.plasmid_implementations[0].wasGeneratedBy, + }, + sbol_component=component, + ) diff --git a/tests/unit/sbol/test_assembly_service.py b/tests/unit/sbol/test_assembly_service.py new file mode 100644 index 0000000..2ef8475 --- /dev/null +++ b/tests/unit/sbol/test_assembly_service.py @@ -0,0 +1,81 @@ +import sbol2 +import pytest + +from buildcompiler.domain import ( + BuildStage, + IndexedBackbone, + IndexedPlasmid, + IndexedReagent, + MaterialState, +) +from buildcompiler.sbol import AssemblyJob, AssemblySbolResult, AssemblyService + + +def test_assembly_service_runs_and_returns_normalized_products(monkeypatch): + source = sbol2.Document() + product_component = sbol2.ComponentDefinition("assembled_product") + source.add(product_component) + product_impl = sbol2.Implementation("assembled_product_impl") + product_impl.built = product_component.identity + source.add(product_impl) + product_impl.wasGeneratedBy = "https://example.org/activity/assembly" + + class FakeLegacyAssembly: + def __init__(self, **kwargs): + self.assembly_activity = sbol2.Activity("fake_assembly") + + def run(self, include_extracted_parts=False): + return [type("LegacyProduct", (), {"plasmid_definition": product_component, "plasmid_implementations": [product_impl]})()], source + + monkeypatch.setattr("buildcompiler.sbol.assembly.Assembly", FakeLegacyAssembly) + + service = AssemblyService() + result = service.run( + AssemblyJob( + stage=BuildStage.ASSEMBLY_LVL1, + product_identity="https://example.org/products/p001", + product_display_id="p001", + part_plasmids=[ + IndexedPlasmid( + identity=product_component.identity, + sbol_component=product_component, + metadata={"implementation_identity": product_impl.identity}, + ) + ], + backbone=IndexedBackbone( + identity=product_component.identity, + sbol_component=product_component, + metadata={"implementation_identity": product_impl.identity}, + ), + restriction_enzyme=IndexedReagent(identity=product_impl.identity), + ligase=IndexedReagent(identity=product_impl.identity), + source_document=source, + target_document=sbol2.Document(), + ) + ) + + assert isinstance(result, AssemblySbolResult) + assert result.products + assert isinstance(result.products[0], IndexedPlasmid) + assert result.products[0].state == MaterialState.GENERATED + assert result.activity_identity + + +def test_assembly_service_raises_clear_error_for_missing_component(): + doc = sbol2.Document() + service = AssemblyService() + + with pytest.raises(ValueError, match="Missing SBOL ComponentDefinition"): + service.run( + AssemblyJob( + stage=BuildStage.ASSEMBLY_LVL1, + product_identity="https://example.org/products/p001", + product_display_id="p001", + part_plasmids=[IndexedPlasmid(identity="https://example.org/missing")], + backbone=IndexedBackbone(identity="https://example.org/backbone"), + restriction_enzyme=IndexedReagent(identity="https://example.org/reagent/re"), + ligase=IndexedReagent(identity="https://example.org/reagent/ligase"), + source_document=doc, + target_document=sbol2.Document(), + ) + ) From e4e65169aaf7a43a3d1192a65e2e38a347063765 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 22:59:50 -0600 Subject: [PATCH 52/93] Implement lvl1 assembly stage orchestration and PUDU JSON adapter --- src/buildcompiler/adapters/pudu/__init__.py | 6 +- .../adapters/pudu/assembly_json.py | 64 +++++ src/buildcompiler/stages/__init__.py | 6 +- src/buildcompiler/stages/assembly_lvl1.py | 236 ++++++++++++++++++ .../unit/adapters/pudu/test_assembly_json.py | 26 ++ tests/unit/stages/test_assembly_lvl1.py | 169 +++++++++++++ 6 files changed, 505 insertions(+), 2 deletions(-) create mode 100644 src/buildcompiler/adapters/pudu/assembly_json.py create mode 100644 src/buildcompiler/stages/assembly_lvl1.py create mode 100644 tests/unit/adapters/pudu/test_assembly_json.py create mode 100644 tests/unit/stages/test_assembly_lvl1.py diff --git a/src/buildcompiler/adapters/pudu/__init__.py b/src/buildcompiler/adapters/pudu/__init__.py index 5e19692..a31fbde 100644 --- a/src/buildcompiler/adapters/pudu/__init__.py +++ b/src/buildcompiler/adapters/pudu/__init__.py @@ -1 +1,5 @@ -"""Package scaffolding for clean architecture.""" +"""PUDU adapter exports.""" + +from .assembly_json import assembly_route_to_pudu_json, assembly_routes_to_pudu_json + +__all__ = ["assembly_route_to_pudu_json", "assembly_routes_to_pudu_json"] diff --git a/src/buildcompiler/adapters/pudu/assembly_json.py b/src/buildcompiler/adapters/pudu/assembly_json.py new file mode 100644 index 0000000..9d5fbe8 --- /dev/null +++ b/src/buildcompiler/adapters/pudu/assembly_json.py @@ -0,0 +1,64 @@ +"""In-memory adapter for compiler-level PUDU assembly JSON payloads.""" + +from collections.abc import Sequence + +from buildcompiler.domain import IndexedBackbone, IndexedPlasmid, IndexedReagent + + +def _stable_identifier(identity: str, display_id: str | None) -> str: + return identity or display_id or "" + + +def assembly_route_to_pudu_json( + *, + product_identity: str, + part_plasmids: Sequence[IndexedPlasmid], + backbone: IndexedBackbone, + restriction_enzyme: IndexedReagent, +) -> dict[str, object]: + """Adapt a selected lvl1 route into legacy-compatible assembly JSON keys.""" + + parts_list = [ + _stable_identifier(identity=part.identity, display_id=part.display_id) + for part in part_plasmids + ] + return { + "Product": product_identity, + "Backbone": _stable_identifier( + identity=backbone.identity, display_id=backbone.display_id + ), + "PartsList": parts_list, + "Restriction Enzyme": ( + restriction_enzyme.name + or _stable_identifier( + identity=restriction_enzyme.identity, + display_id=restriction_enzyme.display_id, + ) + ), + } + + +def assembly_routes_to_pudu_json( + *, + product_identities: Sequence[str], + part_plasmid_routes: Sequence[Sequence[IndexedPlasmid]], + backbones: Sequence[IndexedBackbone], + restriction_enzymes: Sequence[IndexedReagent], +) -> list[dict[str, object]]: + """Batch helper for deterministic in-memory assembly JSON payloads.""" + + return [ + assembly_route_to_pudu_json( + product_identity=product_identity, + part_plasmids=part_plasmids, + backbone=backbone, + restriction_enzyme=restriction_enzyme, + ) + for product_identity, part_plasmids, backbone, restriction_enzyme in zip( + product_identities, + part_plasmid_routes, + backbones, + restriction_enzymes, + strict=True, + ) + ] diff --git a/src/buildcompiler/stages/__init__.py b/src/buildcompiler/stages/__init__.py index 5e19692..b024f42 100644 --- a/src/buildcompiler/stages/__init__.py +++ b/src/buildcompiler/stages/__init__.py @@ -1 +1,5 @@ -"""Package scaffolding for clean architecture.""" +"""Stage exports.""" + +from .assembly_lvl1 import AssemblyLvl1Stage + +__all__ = ["AssemblyLvl1Stage"] diff --git a/src/buildcompiler/stages/assembly_lvl1.py b/src/buildcompiler/stages/assembly_lvl1.py new file mode 100644 index 0000000..9863565 --- /dev/null +++ b/src/buildcompiler/stages/assembly_lvl1.py @@ -0,0 +1,236 @@ +"""Thin lvl1 assembly stage orchestration.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +import sbol2 + +from buildcompiler.adapters.pudu import assembly_route_to_pudu_json +from buildcompiler.api import BuildOptions +from buildcompiler.domain import ( + BuildRequest, + BuildStage, + BuildWarning, + MissingBuildInput, + StageResult, + StageStatus, +) +from buildcompiler.inventory import CompatibilitySelector, Inventory +from buildcompiler.sbol import AssemblyJob, AssemblyService + + +class AssemblyLvl1Stage: + def __init__( + self, + *, + inventory: Inventory, + selector: CompatibilitySelector | None = None, + assembly_service: AssemblyService | None = None, + options: BuildOptions | None = None, + ) -> None: + self.inventory = inventory + self.options = options or BuildOptions() + self.selector = selector or CompatibilitySelector( + inventory, options=self.options + ) + self.assembly_service = assembly_service or AssemblyService() + + def run( + self, + request: BuildRequest, + *, + source_document: sbol2.Document, + target_document: sbol2.Document, + ) -> StageResult: + constraints = request.constraints or {} + warnings = self._extract_warnings(request=request, constraints=constraints) + part_identities = self._extract_part_identities(constraints) + if not part_identities: + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL1.value}", + stage=BuildStage.ASSEMBLY_LVL1, + status=StageStatus.FAILED, + request_ids=[request.id], + warnings=warnings, + logs=[ + "Missing ordered_part_identities/part_identities constraint for lvl1 assembly." + ], + ) + + route_selection = self.selector.select_lvl1_route( + request_id=request.id, + part_identities=part_identities, + constraints=constraints, + ) + route = route_selection.selected + if route is None: + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL1.value}", + stage=BuildStage.ASSEMBLY_LVL1, + status=StageStatus.BLOCKED, + request_ids=[request.id], + warnings=warnings, + logs=["No lvl1 route selected by CompatibilitySelector."], + ) + + missing_inputs: list[MissingBuildInput] = [] + for missing_identity in route.missing_part_identities: + missing_inputs.append( + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL1, + source_design_identity=request.source_identity, + missing_identity=missing_identity, + missing_display_id=missing_identity.rsplit("/", 1)[-1], + missing_kind=self._infer_missing_kind(missing_identity), + required_stage=BuildStage.DOMESTICATION, + reason="No compatible lvl1 part plasmid found in inventory.", + ) + ) + + if route.backbone is None: + missing_inputs.append( + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL1, + source_design_identity=request.source_identity, + missing_identity="backbone", + missing_display_id=None, + missing_kind="backbone", + required_stage="fatal", + reason="No compatible lvl1 backbone found in inventory.", + ) + ) + + restriction_enzyme_name = self.options.reagents.default_restriction_enzyme + restriction_enzyme = self.inventory.find_restriction_enzyme( + restriction_enzyme_name + ) + if restriction_enzyme is None: + missing_inputs.append( + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL1, + source_design_identity=request.source_identity, + missing_identity=restriction_enzyme_name, + missing_display_id=restriction_enzyme_name, + missing_kind="restriction_enzyme", + required_stage="fatal", + reason="Required restriction enzyme is missing from inventory.", + ) + ) + + ligase_name = self.options.reagents.default_ligase + ligase = self.inventory.find_ligase(ligase_name) + if ligase is None: + missing_inputs.append( + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL1, + source_design_identity=request.source_identity, + missing_identity=ligase_name, + missing_display_id=ligase_name, + missing_kind="ligase", + required_stage="fatal", + reason="Required ligase is missing from inventory.", + ) + ) + + if missing_inputs: + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL1.value}", + stage=BuildStage.ASSEMBLY_LVL1, + status=StageStatus.BLOCKED, + request_ids=[request.id], + missing_inputs=missing_inputs, + warnings=warnings, + logs=[ + f"Blocked lvl1 assembly for {request.id}; missing {len(missing_inputs)} required input(s)." + ], + ) + + product_identity = ( + constraints.get("product_identity") or request.source_identity + ) + product_display_id = ( + constraints.get("product_display_id") + or request.source_display_id + or product_identity.rsplit("/", 1)[-1] + ) + + assembly_result = self.assembly_service.run( + AssemblyJob( + stage=BuildStage.ASSEMBLY_LVL1, + product_identity=product_identity, + product_display_id=product_display_id, + part_plasmids=list(route.selected_part_plasmids), + backbone=route.backbone, + restriction_enzyme=restriction_enzyme, + ligase=ligase, + source_document=source_document, + target_document=target_document, + ) + ) + + for product in assembly_result.products: + insert_identities = list(product.metadata.get("insert_identities", [])) + if request.source_identity not in insert_identities: + insert_identities.append(request.source_identity) + product.metadata["insert_identities"] = insert_identities + self.inventory.add_generated_product(product) + + json_intermediate = assembly_route_to_pudu_json( + product_identity=product_identity, + part_plasmids=route.selected_part_plasmids, + backbone=route.backbone, + restriction_enzyme=restriction_enzyme, + ) + + logs = [ + f"Selected lvl1 route with {len(route.selected_part_plasmids)} part plasmid(s).", + *assembly_result.logs, + ] + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL1.value}", + stage=BuildStage.ASSEMBLY_LVL1, + status=StageStatus.SUCCESS, + request_ids=[request.id], + products=assembly_result.products, + warnings=warnings, + sbol_document=assembly_result.stage_document, + json_intermediate=json_intermediate, + logs=logs, + ) + + def _extract_part_identities(self, constraints: Mapping[str, Any]) -> list[str]: + ordered = constraints.get("ordered_part_identities") + if ordered: + return list(ordered) + unordered = constraints.get("part_identities") + if unordered: + return list(unordered) + return [] + + def _extract_warnings( + self, *, request: BuildRequest, constraints: Mapping[str, Any] + ) -> list[BuildWarning]: + warnings: list[BuildWarning] = [] + for item in constraints.get("ordering_warnings", []): + if isinstance(item, BuildWarning): + warnings.append(item) + elif isinstance(item, Mapping): + warnings.append( + BuildWarning( + code=str(item.get("code", "ordering_warning")), + message=str(item.get("message", "Ordering warning.")), + stage=BuildStage.ASSEMBLY_LVL1, + source_identity=request.source_identity, + metadata=dict(item.get("metadata", {})), + ) + ) + return warnings + + def _infer_missing_kind(self, part_identity: str) -> str: + text = part_identity.lower() + for role in ("promoter", "rbs", "cds", "terminator"): + if role in text: + return role + return "reagent" diff --git a/tests/unit/adapters/pudu/test_assembly_json.py b/tests/unit/adapters/pudu/test_assembly_json.py new file mode 100644 index 0000000..5b0859c --- /dev/null +++ b/tests/unit/adapters/pudu/test_assembly_json.py @@ -0,0 +1,26 @@ +from buildcompiler.adapters.pudu import assembly_route_to_pudu_json +from buildcompiler.domain import IndexedBackbone, IndexedPlasmid, IndexedReagent + + +def test_assembly_route_to_pudu_json_shape_and_values(): + payload = assembly_route_to_pudu_json( + product_identity="https://example.org/products/p1", + part_plasmids=[ + IndexedPlasmid(identity="https://example.org/plasmids/part1"), + IndexedPlasmid(identity="https://example.org/plasmids/part2"), + ], + backbone=IndexedBackbone(identity="https://example.org/backbones/b1"), + restriction_enzyme=IndexedReagent( + identity="https://example.org/reagents/re1", name="BsaI" + ), + ) + + assert payload == { + "Product": "https://example.org/products/p1", + "Backbone": "https://example.org/backbones/b1", + "PartsList": [ + "https://example.org/plasmids/part1", + "https://example.org/plasmids/part2", + ], + "Restriction Enzyme": "BsaI", + } diff --git a/tests/unit/stages/test_assembly_lvl1.py b/tests/unit/stages/test_assembly_lvl1.py new file mode 100644 index 0000000..bfbf70b --- /dev/null +++ b/tests/unit/stages/test_assembly_lvl1.py @@ -0,0 +1,169 @@ +import sbol2 + +from buildcompiler.domain import ( + BuildRequest, + BuildStage, + BuildWarning, + DesignKind, + IndexedBackbone, + IndexedPlasmid, + IndexedReagent, + MaterialState, + StageStatus, +) +from buildcompiler.inventory import Inventory +from buildcompiler.sbol import AssemblySbolResult +from buildcompiler.stages import AssemblyLvl1Stage + + +class _FakeAssemblyService: + def __init__(self, products): + self.products = products + + def run(self, job): + return AssemblySbolResult( + products=self.products, + stage_document=job.target_document, + activity_identity="https://example.org/activity/a1", + logs=["fake-assembly-service-ran"], + ) + + +def _inventory(*, include_parts=True, include_backbone=True, include_reagents=True): + plasmids = [] + if include_parts: + plasmids = [ + IndexedPlasmid( + identity="https://example.org/plasmids/p-prom", + metadata={ + "insert_identities": ["https://example.org/parts/promoter"], + "antibiotic": "Ampicillin", + }, + ), + IndexedPlasmid( + identity="https://example.org/plasmids/p-rbs", + metadata={ + "insert_identities": ["https://example.org/parts/rbs"], + "antibiotic": "Ampicillin", + }, + ), + IndexedPlasmid( + identity="https://example.org/plasmids/p-cds", + metadata={ + "insert_identities": ["https://example.org/parts/cds"], + "antibiotic": "Ampicillin", + }, + ), + IndexedPlasmid( + identity="https://example.org/plasmids/p-term", + metadata={ + "insert_identities": ["https://example.org/parts/terminator"], + "antibiotic": "Ampicillin", + }, + ), + ] + backbones = [] + if include_backbone: + backbones = [ + IndexedBackbone( + identity="https://example.org/backbones/lvl1", + metadata={ + "fusion_sites": ("A", "B"), + "antibiotic": "Ampicillin", + "stage": BuildStage.ASSEMBLY_LVL1.value, + }, + ) + ] + reagents = [] + if include_reagents: + reagents = [ + IndexedReagent( + identity="https://example.org/reagents/bsaI", + name="BsaI", + reagent_type="restriction_enzyme", + ), + IndexedReagent( + identity="https://example.org/reagents/ligase", + name="T4_DNA_ligase", + reagent_type="ligase", + ), + ] + return Inventory(plasmids=plasmids, backbones=backbones, reagents=reagents) + + +def _request(): + return BuildRequest( + id="req-1", + stage=BuildStage.ASSEMBLY_LVL1, + source_identity="https://example.org/designs/d1", + source_display_id="d1", + source_kind=DesignKind.COMPONENT_DEFINITION, + constraints={ + "ordered_part_identities": [ + "https://example.org/parts/promoter", + "https://example.org/parts/rbs", + "https://example.org/parts/cds", + "https://example.org/parts/terminator", + ], + "fusion_sites": ["A", "B"], + "antibiotic": "Ampicillin", + "ordering_warnings": [ + { + "code": "planner.ordering", + "message": "planner warning", + "metadata": {"source": "planner"}, + } + ], + }, + ) + + +def test_assembly_lvl1_success_returns_products_json_and_indexes_generated(): + inv = _inventory() + generated = IndexedPlasmid( + identity="https://example.org/plasmids/generated-1", + state=MaterialState.GENERATED, + metadata={"insert_identities": ["https://example.org/parts/promoter"]}, + ) + stage = AssemblyLvl1Stage( + inventory=inv, assembly_service=_FakeAssemblyService([generated]) + ) + + result = stage.run( + _request(), source_document=sbol2.Document(), target_document=sbol2.Document() + ) + + assert result.status == StageStatus.SUCCESS + assert result.products + assert result.products[0].identity == generated.identity + assert ( + result.json_intermediate + and result.json_intermediate["Product"] == "https://example.org/designs/d1" + ) + assert result.sbol_document is not None + assert inv.find_lvl1_region_plasmids("https://example.org/designs/d1") + assert result.logs + assert result.warnings and isinstance(result.warnings[0], BuildWarning) + + +def test_assembly_lvl1_missing_inputs_return_blocked_with_structured_kinds(): + inv = _inventory( + include_parts=False, include_backbone=False, include_reagents=False + ) + stage = AssemblyLvl1Stage(inventory=inv, assembly_service=_FakeAssemblyService([])) + + result = stage.run( + _request(), source_document=sbol2.Document(), target_document=sbol2.Document() + ) + + assert result.status == StageStatus.BLOCKED + kinds = {missing.missing_kind for missing in result.missing_inputs} + assert { + "promoter", + "rbs", + "cds", + "terminator", + "backbone", + "restriction_enzyme", + "ligase", + }.issubset(kinds) From dd0ed63a88befd647cdf5d6f225a42928aa73f38 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 23:09:06 -0600 Subject: [PATCH 53/93] Fix lvl1 stage to accept planner part_order constraints --- src/buildcompiler/stages/assembly_lvl1.py | 3 +++ tests/unit/stages/test_assembly_lvl1.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/buildcompiler/stages/assembly_lvl1.py b/src/buildcompiler/stages/assembly_lvl1.py index 9863565..3332eb7 100644 --- a/src/buildcompiler/stages/assembly_lvl1.py +++ b/src/buildcompiler/stages/assembly_lvl1.py @@ -204,6 +204,9 @@ def _extract_part_identities(self, constraints: Mapping[str, Any]) -> list[str]: ordered = constraints.get("ordered_part_identities") if ordered: return list(ordered) + planner_order = constraints.get("part_order") + if planner_order: + return list(planner_order) unordered = constraints.get("part_identities") if unordered: return list(unordered) diff --git a/tests/unit/stages/test_assembly_lvl1.py b/tests/unit/stages/test_assembly_lvl1.py index bfbf70b..1563525 100644 --- a/tests/unit/stages/test_assembly_lvl1.py +++ b/tests/unit/stages/test_assembly_lvl1.py @@ -167,3 +167,20 @@ def test_assembly_lvl1_missing_inputs_return_blocked_with_structured_kinds(): "restriction_enzyme", "ligase", }.issubset(kinds) + + +def test_assembly_lvl1_accepts_planner_part_order_constraint(): + inv = _inventory() + stage = AssemblyLvl1Stage(inventory=inv, assembly_service=_FakeAssemblyService([])) + request = _request() + request.constraints = { + "part_order": request.constraints["ordered_part_identities"], + "fusion_sites": request.constraints["fusion_sites"], + "antibiotic": request.constraints["antibiotic"], + } + + result = stage.run( + request, source_document=sbol2.Document(), target_document=sbol2.Document() + ) + + assert result.status == StageStatus.SUCCESS From eb16ac7dd8fe86f57ddc313597c30270a776d3c0 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 23:32:00 -0600 Subject: [PATCH 54/93] Implement domestication planner, service, and stage with approvals --- src/buildcompiler/planning/__init__.py | 10 +- src/buildcompiler/planning/domestication.py | 83 ++++++++++ src/buildcompiler/sbol/__init__.py | 4 + src/buildcompiler/sbol/domestication.py | 78 +++++++++ src/buildcompiler/stages/__init__.py | 3 +- src/buildcompiler/stages/domestication.py | 149 ++++++++++++++++++ .../planning/test_domestication_planner.py | 52 ++++++ tests/unit/sbol/test_domestication_service.py | 34 ++++ tests/unit/stages/test_domestication_stage.py | 71 +++++++++ 9 files changed, 482 insertions(+), 2 deletions(-) create mode 100644 src/buildcompiler/planning/domestication.py create mode 100644 src/buildcompiler/sbol/domestication.py create mode 100644 src/buildcompiler/stages/domestication.py create mode 100644 tests/unit/planning/test_domestication_planner.py create mode 100644 tests/unit/sbol/test_domestication_service.py create mode 100644 tests/unit/stages/test_domestication_stage.py diff --git a/src/buildcompiler/planning/__init__.py b/src/buildcompiler/planning/__init__.py index 5dd1576..483aae4 100644 --- a/src/buildcompiler/planning/__init__.py +++ b/src/buildcompiler/planning/__init__.py @@ -1,6 +1,14 @@ """Planning package exports.""" +from .domestication import DomesticationPlan, DomesticationPlanner, SequenceEditProposal from .full_build_planner import FullBuildPlanner from .models import BuildPlan, UnsupportedPlanningRecord -__all__ = ["BuildPlan", "UnsupportedPlanningRecord", "FullBuildPlanner"] +__all__ = [ + "BuildPlan", + "UnsupportedPlanningRecord", + "FullBuildPlanner", + "DomesticationPlan", + "DomesticationPlanner", + "SequenceEditProposal", +] diff --git a/src/buildcompiler/planning/domestication.py b/src/buildcompiler/planning/domestication.py new file mode 100644 index 0000000..00aea97 --- /dev/null +++ b/src/buildcompiler/planning/domestication.py @@ -0,0 +1,83 @@ +"""Deterministic domestication planning helpers.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + +import sbol2 + +from buildcompiler.domain import BuildStage, BuildWarning +from buildcompiler.planning.validation import classify_part_role + + +@dataclass +class SequenceEditProposal: + source_identity: str + enzyme_name: str + site_sequence: str + position: int + original_sequence: str + proposed_sequence: str + reason: str + approved: bool = False + + +@dataclass +class DomesticationPlan: + part_identity: str + part_display_id: str | None + part_role: str + backbone_identity: str | None = None + restriction_enzyme_name: str = "BsaI" + ligase_name: str = "T4_DNA_ligase" + sequence_edit_proposals: list[SequenceEditProposal] = field(default_factory=list) + warnings: list[BuildWarning] = field(default_factory=list) + + +class DomesticationPlanner: + """Pure planner for domestication requirements for a single part.""" + + _BSAI_SITES = ("GGTCTC", "GAGACC") + + def plan(self, part_component: sbol2.ComponentDefinition) -> DomesticationPlan: + part_role = classify_part_role(part_component) + if part_role is None: + raise ValueError( + f"Unsupported domestication role for part {part_component.identity}; expected promoter/rbs/cds/terminator" + ) + + sequence = self._resolve_sequence(part_component) + proposals: list[SequenceEditProposal] = [] + for site in self._BSAI_SITES: + start = 0 + while True: + index = sequence.find(site, start) + if index < 0: + break + proposals.append( + SequenceEditProposal( + source_identity=part_component.identity, + enzyme_name="BsaI", + site_sequence=site, + position=index, + original_sequence=site, + proposed_sequence=f"{site[:-1]}A", + reason="Internal BsaI recognition site detected; human-reviewed edit required.", + ) + ) + start = index + 1 + + return DomesticationPlan( + part_identity=part_component.identity, + part_display_id=part_component.displayId, + part_role=part_role, + sequence_edit_proposals=proposals, + ) + + def _resolve_sequence(self, part_component: sbol2.ComponentDefinition) -> str: + for sequence_ref in part_component.sequences: + sequence_obj = part_component.doc.find(sequence_ref) if part_component.doc else None + elements = getattr(sequence_obj, "elements", None) + if isinstance(elements, str) and elements: + return elements.upper() + raise ValueError(f"Part {part_component.identity} is missing a usable DNA sequence") diff --git a/src/buildcompiler/sbol/__init__.py b/src/buildcompiler/sbol/__init__.py index d9e6f2c..7539882 100644 --- a/src/buildcompiler/sbol/__init__.py +++ b/src/buildcompiler/sbol/__init__.py @@ -1,12 +1,16 @@ """SBOL package exports for clean architecture contracts.""" from .assembly import AssemblyJob, AssemblySbolResult, AssemblyService +from .domestication import DomesticationJob, DomesticationSbolResult, DomesticationService from .resolver import PullPolicy, SbolResolver __all__ = [ "AssemblyJob", "AssemblySbolResult", "AssemblyService", + "DomesticationJob", + "DomesticationSbolResult", + "DomesticationService", "PullPolicy", "SbolResolver", ] diff --git a/src/buildcompiler/sbol/domestication.py b/src/buildcompiler/sbol/domestication.py new file mode 100644 index 0000000..f13bb36 --- /dev/null +++ b/src/buildcompiler/sbol/domestication.py @@ -0,0 +1,78 @@ +"""SBOL domestication service.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +import sbol2 + +from buildcompiler.domain import IndexedBackbone, IndexedPlasmid, IndexedReagent, MaterialState + + +@dataclass +class DomesticationJob: + part_identity: str + part_display_id: str | None + part_component: Any + backbone: IndexedBackbone + restriction_enzyme: IndexedReagent + ligase: IndexedReagent + source_document: Any + target_document: Any + sequence_edit_proposals: list[Any] = field(default_factory=list) + + +@dataclass +class DomesticationSbolResult: + product: IndexedPlasmid + stage_document: Any + artifacts: dict[str, Any] = field(default_factory=dict) + logs: list[str] = field(default_factory=list) + + +class DomesticationService: + def run(self, job: DomesticationJob) -> DomesticationSbolResult: + component = self._ensure_component(job.part_component) + product_identity = f"{component.identity}/domesticated" + product_display_id = f"{job.part_display_id or component.displayId or component.identity.rsplit('/', 1)[-1]}_lvl0" + + product_component = sbol2.ComponentDefinition(product_identity) + product_component.displayId = product_display_id + product_component.name = f"Domesticated {component.displayId or component.identity.rsplit('/', 1)[-1]}" + for role in component.roles: + product_component.roles = role + job.target_document.addComponentDefinition(product_component) + + metadata = { + "source_stage": "domestication", + "source_part_identity": job.part_identity, + "insert_identities": [job.part_identity], + "backbone_identity": job.backbone.identity, + "restriction_enzyme": { + "identity": job.restriction_enzyme.identity, + "name": job.restriction_enzyme.name, + }, + "ligase": {"identity": job.ligase.identity, "name": job.ligase.name}, + "sequence_edit_proposals": [proposal.__dict__.copy() for proposal in job.sequence_edit_proposals], + } + product = IndexedPlasmid( + identity=product_identity, + display_id=product_display_id, + name=product_component.name, + state=MaterialState.GENERATED, + roles=list(component.roles), + metadata=metadata, + sbol_component=product_component, + ) + return DomesticationSbolResult( + product=product, + stage_document=job.target_document, + artifacts={"domestication": metadata}, + logs=[f"Generated domesticated lvl0 product {product_identity}."], + ) + + def _ensure_component(self, component: Any) -> sbol2.ComponentDefinition: + if not isinstance(component, sbol2.ComponentDefinition): + raise ValueError("DomesticationJob.part_component must be an sbol2.ComponentDefinition") + return component diff --git a/src/buildcompiler/stages/__init__.py b/src/buildcompiler/stages/__init__.py index b024f42..d354b34 100644 --- a/src/buildcompiler/stages/__init__.py +++ b/src/buildcompiler/stages/__init__.py @@ -1,5 +1,6 @@ """Stage exports.""" from .assembly_lvl1 import AssemblyLvl1Stage +from .domestication import DomesticationStage -__all__ = ["AssemblyLvl1Stage"] +__all__ = ["AssemblyLvl1Stage", "DomesticationStage"] diff --git a/src/buildcompiler/stages/domestication.py b/src/buildcompiler/stages/domestication.py new file mode 100644 index 0000000..62e9618 --- /dev/null +++ b/src/buildcompiler/stages/domestication.py @@ -0,0 +1,149 @@ +"""Domestication stage orchestration.""" + +from __future__ import annotations + +import sbol2 + +from buildcompiler.api import BuildOptions, ProtocolMode +from buildcompiler.domain import ( + ApprovalStatus, + BuildRequest, + BuildStage, + BuildWarning, + MissingBuildInput, + RequiredApproval, + StageResult, + StageStatus, +) +from buildcompiler.inventory import Inventory +from buildcompiler.planning.domestication import DomesticationPlanner +from buildcompiler.sbol.domestication import DomesticationJob, DomesticationService + + +class DomesticationStage: + def __init__( + self, + *, + inventory: Inventory, + domestication_planner: DomesticationPlanner | None = None, + domestication_service: DomesticationService | None = None, + options: BuildOptions | None = None, + ) -> None: + self.inventory = inventory + self.domestication_planner = domestication_planner or DomesticationPlanner() + self.domestication_service = domestication_service or DomesticationService() + self.options = options or BuildOptions() + + def run(self, request: BuildRequest, *, source_document: sbol2.Document, target_document: sbol2.Document) -> StageResult: + part_component = source_document.find(request.source_identity) + if not isinstance(part_component, sbol2.ComponentDefinition): + for candidate in source_document.componentDefinitions: + if ( + candidate.identity == request.source_identity + or candidate.persistentIdentity == request.source_identity + or candidate.displayId == request.source_identity + or candidate.identity.endswith(f"/{request.source_identity}/1") + or candidate.persistentIdentity.endswith(f"/{request.source_identity}") + ): + part_component = candidate + break + if not isinstance(part_component, sbol2.ComponentDefinition): + return StageResult( + id=f"{request.id}:{BuildStage.DOMESTICATION.value}", + stage=BuildStage.DOMESTICATION, + status=StageStatus.FAILED, + request_ids=[request.id], + logs=[f"Failed domestication: source part {request.source_identity} not found."], + ) + try: + plan = self.domestication_planner.plan(part_component) + except ValueError as exc: + return StageResult( + id=f"{request.id}:{BuildStage.DOMESTICATION.value}", + stage=BuildStage.DOMESTICATION, + status=StageStatus.FAILED, + request_ids=[request.id], + warnings=[BuildWarning(code="domestication.invalid_input", message=str(exc), stage=BuildStage.DOMESTICATION, source_identity=request.source_identity)], + logs=[str(exc)], + ) + + missing_inputs: list[MissingBuildInput] = [] + backbone = self.inventory.find_backbone(stage=BuildStage.DOMESTICATION) + if backbone is None: + missing_inputs.append(MissingBuildInput(BuildStage.DOMESTICATION, request.source_identity, "backbone", None, "backbone", "fatal", "No domestication backbone found in inventory.")) + + restriction = self.inventory.find_restriction_enzyme(self.options.reagents.default_restriction_enzyme) + if restriction is None: + missing_inputs.append(MissingBuildInput(BuildStage.DOMESTICATION, request.source_identity, self.options.reagents.default_restriction_enzyme, self.options.reagents.default_restriction_enzyme, "restriction_enzyme", "fatal", "Required domestication restriction enzyme missing from inventory.")) + + ligase = self.inventory.find_ligase(self.options.reagents.default_ligase) + if ligase is None: + missing_inputs.append(MissingBuildInput(BuildStage.DOMESTICATION, request.source_identity, self.options.reagents.default_ligase, self.options.reagents.default_ligase, "ligase", "fatal", "Required domestication ligase missing from inventory.")) + + if missing_inputs: + return StageResult( + id=f"{request.id}:{BuildStage.DOMESTICATION.value}", + stage=BuildStage.DOMESTICATION, + status=StageStatus.BLOCKED, + request_ids=[request.id], + missing_inputs=missing_inputs, + logs=["Domestication blocked on missing backbone/reagents."], + protocol_artifacts={"sequence_edit_proposals": [proposal.__dict__.copy() for proposal in plan.sequence_edit_proposals]}, + ) + + approvals: list[RequiredApproval] = [] + if plan.sequence_edit_proposals: + approval_id = f"domestication-edit:{request.source_identity}" + process_approved = "domestication_sequence_edit" in self.options.approvals.approved_processes + id_approved = approval_id in self.options.approvals.approved_approval_ids + allow_edits = self.options.domestication.allow_sequence_domestication_edits + protocol_mode_active = self.options.protocol.mode != ProtocolMode.NONE + if (not allow_edits) or (protocol_mode_active and not (process_approved or id_approved)): + approvals.append( + RequiredApproval( + status=ApprovalStatus.REQUIRED, + process="domestication_sequence_edit", + reason="Sequence edits were proposed and require explicit human approval.", + metadata={ + "approval_id": approval_id, + "part_identity": request.source_identity, + "proposals": [proposal.__dict__.copy() for proposal in plan.sequence_edit_proposals], + }, + ) + ) + + if approvals: + return StageResult( + id=f"{request.id}:{BuildStage.DOMESTICATION.value}", + stage=BuildStage.DOMESTICATION, + status=StageStatus.BLOCKED, + request_ids=[request.id], + required_approvals=approvals, + protocol_artifacts={"sequence_edit_proposals": [proposal.__dict__.copy() for proposal in plan.sequence_edit_proposals]}, + logs=["Domestication blocked pending sequence-edit approval."], + ) + + result = self.domestication_service.run( + DomesticationJob( + part_identity=request.source_identity, + part_display_id=request.source_display_id, + part_component=part_component, + backbone=backbone, + restriction_enzyme=restriction, + ligase=ligase, + source_document=source_document, + target_document=target_document, + sequence_edit_proposals=plan.sequence_edit_proposals, + ) + ) + self.inventory.add_generated_product(result.product) + return StageResult( + id=f"{request.id}:{BuildStage.DOMESTICATION.value}", + stage=BuildStage.DOMESTICATION, + status=StageStatus.SUCCESS, + request_ids=[request.id], + products=[result.product], + sbol_document=result.stage_document, + protocol_artifacts={"sequence_edit_proposals": [proposal.__dict__.copy() for proposal in plan.sequence_edit_proposals], **result.artifacts}, + logs=result.logs, + ) diff --git a/tests/unit/planning/test_domestication_planner.py b/tests/unit/planning/test_domestication_planner.py new file mode 100644 index 0000000..031c1dd --- /dev/null +++ b/tests/unit/planning/test_domestication_planner.py @@ -0,0 +1,52 @@ +import sbol2 +import pytest + +from buildcompiler.constants import PART_ROLES +from buildcompiler.planning import DomesticationPlanner + + +def _part(identity: str, role: str, sequence: str | None = None) -> sbol2.ComponentDefinition: + doc = sbol2.Document() + part = sbol2.ComponentDefinition(identity) + part.roles = role + doc.addComponentDefinition(part) + if sequence is not None: + seq = sbol2.Sequence(f"{identity}_seq") + seq.elements = sequence + seq.encoding = sbol2.SBOL_ENCODING_IUPAC + doc.addSequence(seq) + part.sequences = seq.identity + return part + + +def test_supported_role_produces_plan() -> None: + planner = DomesticationPlanner() + part = _part("https://example.org/p", sorted(PART_ROLES)[0], "ATGCGT") + plan = planner.plan(part) + assert plan.part_identity == part.identity + assert plan.part_role in {"promoter", "rbs", "cds", "terminator"} + + +def test_unsupported_role_fails_structurally() -> None: + planner = DomesticationPlanner() + part = _part("https://example.org/x", "https://example.org/unsupported", "ATGC") + with pytest.raises(ValueError, match="Unsupported domestication role"): + planner.plan(part) + + +def test_missing_sequence_fails() -> None: + planner = DomesticationPlanner() + part = _part("https://example.org/p2", sorted(PART_ROLES)[0]) + with pytest.raises(ValueError, match="missing a usable DNA sequence"): + planner.plan(part) + + +def test_bsai_sites_create_edit_proposals_without_mutating_sequence() -> None: + planner = DomesticationPlanner() + original = "AAAGGTCTCTTT" + part = _part("https://example.org/p3", sorted(PART_ROLES)[0], original) + plan = planner.plan(part) + assert len(plan.sequence_edit_proposals) == 1 + assert plan.sequence_edit_proposals[0].site_sequence == "GGTCTC" + seq = part.doc.find(part.sequences[0]) + assert seq.elements == original diff --git a/tests/unit/sbol/test_domestication_service.py b/tests/unit/sbol/test_domestication_service.py new file mode 100644 index 0000000..765c63e --- /dev/null +++ b/tests/unit/sbol/test_domestication_service.py @@ -0,0 +1,34 @@ +import sbol2 + +from buildcompiler.domain import IndexedBackbone, IndexedReagent, MaterialState +from buildcompiler.planning import SequenceEditProposal +from buildcompiler.sbol import DomesticationJob, DomesticationService + + +def test_domestication_service_returns_generated_plasmid_with_provenance() -> None: + source = sbol2.Document() + target = sbol2.Document() + part = sbol2.ComponentDefinition("https://example.org/part") + source.addComponentDefinition(part) + + service = DomesticationService() + result = service.run( + DomesticationJob( + part_identity=part.identity, + part_display_id="part", + part_component=part, + backbone=IndexedBackbone("https://example.org/bb", metadata={"stage": "domestication"}), + restriction_enzyme=IndexedReagent("https://example.org/bsai", name="BsaI", reagent_type="restriction_enzyme"), + ligase=IndexedReagent("https://example.org/lig", name="T4_DNA_ligase", reagent_type="ligase"), + source_document=source, + target_document=target, + sequence_edit_proposals=[ + SequenceEditProposal(part.identity, "BsaI", "GGTCTC", 5, "GGTCTC", "GGTCTA", "reason") + ], + ) + ) + + assert result.product.state == MaterialState.GENERATED + assert result.product.metadata["source_part_identity"] == part.identity + assert result.product.metadata["insert_identities"] == [part.identity] + assert result.logs diff --git a/tests/unit/stages/test_domestication_stage.py b/tests/unit/stages/test_domestication_stage.py new file mode 100644 index 0000000..8b7b993 --- /dev/null +++ b/tests/unit/stages/test_domestication_stage.py @@ -0,0 +1,71 @@ +import sbol2 + +from buildcompiler.api import BuildOptions, ProtocolMode +from buildcompiler.domain import BuildRequest, BuildStage, DesignKind, IndexedBackbone, IndexedReagent, StageStatus +from buildcompiler.inventory import Inventory +from buildcompiler.stages import DomesticationStage + + +def _request(identity: str = "part") -> BuildRequest: + return BuildRequest("req-1", BuildStage.DOMESTICATION, identity, "part", DesignKind.COMPONENT_DEFINITION) + + +def _source_doc(identity: str = "part", seq: str = "ATGGGTCTCAA") -> sbol2.Document: + doc = sbol2.Document() + part = sbol2.ComponentDefinition(identity) + part.roles = "http://identifiers.org/so/SO:0000167" + seq_obj = sbol2.Sequence(f"{identity}_seq") + seq_obj.elements = seq + seq_obj.encoding = sbol2.SBOL_ENCODING_IUPAC + doc.addSequence(seq_obj) + part.sequences = seq_obj.identity + doc.addComponentDefinition(part) + return doc + + +def _inventory(with_backbone=True, with_enzyme=True, with_ligase=True) -> Inventory: + backbones = [IndexedBackbone("bb", metadata={"stage": BuildStage.DOMESTICATION.value})] if with_backbone else [] + reagents = [] + if with_enzyme: + reagents.append(IndexedReagent("e1", name="BsaI", reagent_type="restriction_enzyme")) + if with_ligase: + reagents.append(IndexedReagent("l1", name="T4_DNA_ligase", reagent_type="ligase")) + return Inventory(backbones=backbones, reagents=reagents) + + +def test_blocked_when_backbone_missing() -> None: + stage = DomesticationStage(inventory=_inventory(with_backbone=False)) + result = stage.run(_request(), source_document=_source_doc(), target_document=sbol2.Document()) + assert result.status == StageStatus.BLOCKED + assert result.missing_inputs[0].missing_kind == "backbone" + + +def test_blocked_when_reagents_missing() -> None: + stage = DomesticationStage(inventory=_inventory(with_enzyme=False, with_ligase=False)) + result = stage.run(_request(), source_document=_source_doc(), target_document=sbol2.Document()) + kinds = {item.missing_kind for item in result.missing_inputs} + assert result.status == StageStatus.BLOCKED + assert "restriction_enzyme" in kinds + assert "ligase" in kinds + + +def test_default_options_block_for_sequence_edit_approval() -> None: + stage = DomesticationStage(inventory=_inventory()) + result = stage.run(_request(), source_document=_source_doc(), target_document=sbol2.Document()) + assert result.status == StageStatus.BLOCKED + assert result.required_approvals + + +def test_protocol_mode_requires_explicit_process_or_approval_id() -> None: + options = BuildOptions() + options.domestication.allow_sequence_domestication_edits = True + options.protocol.mode = ProtocolMode.MANUAL + stage = DomesticationStage(inventory=_inventory(), options=options) + blocked = stage.run(_request(), source_document=_source_doc(), target_document=sbol2.Document()) + assert blocked.status == StageStatus.BLOCKED + + options.approvals.approved_processes.add("domestication_sequence_edit") + stage2 = DomesticationStage(inventory=_inventory(), options=options) + ok = stage2.run(_request(), source_document=_source_doc(), target_document=sbol2.Document()) + assert ok.status == StageStatus.SUCCESS + assert ok.products[0].metadata["insert_identities"] == ["part"] From b50b13fbe0f20b4b74ae6a27b886ae4a222bbeea Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Tue, 5 May 2026 23:43:51 -0600 Subject: [PATCH 55/93] Fix domestication SBOL implementation metadata and role copying --- src/buildcompiler/sbol/domestication.py | 11 ++++++++--- tests/unit/sbol/test_domestication_service.py | 6 ++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/buildcompiler/sbol/domestication.py b/src/buildcompiler/sbol/domestication.py index f13bb36..2e1014f 100644 --- a/src/buildcompiler/sbol/domestication.py +++ b/src/buildcompiler/sbol/domestication.py @@ -40,14 +40,19 @@ def run(self, job: DomesticationJob) -> DomesticationSbolResult: product_component = sbol2.ComponentDefinition(product_identity) product_component.displayId = product_display_id product_component.name = f"Domesticated {component.displayId or component.identity.rsplit('/', 1)[-1]}" - for role in component.roles: - product_component.roles = role + product_component.roles = list(component.roles) job.target_document.addComponentDefinition(product_component) + implementation_identity = f"{product_identity}_implementation" + product_implementation = sbol2.Implementation(implementation_identity) + product_implementation.built = product_component.identity + job.target_document.addImplementation(product_implementation) + metadata = { "source_stage": "domestication", "source_part_identity": job.part_identity, "insert_identities": [job.part_identity], + "implementation_identity": product_implementation.identity, "backbone_identity": job.backbone.identity, "restriction_enzyme": { "identity": job.restriction_enzyme.identity, @@ -57,7 +62,7 @@ def run(self, job: DomesticationJob) -> DomesticationSbolResult: "sequence_edit_proposals": [proposal.__dict__.copy() for proposal in job.sequence_edit_proposals], } product = IndexedPlasmid( - identity=product_identity, + identity=product_component.identity, display_id=product_display_id, name=product_component.name, state=MaterialState.GENERATED, diff --git a/tests/unit/sbol/test_domestication_service.py b/tests/unit/sbol/test_domestication_service.py index 765c63e..0ddfeae 100644 --- a/tests/unit/sbol/test_domestication_service.py +++ b/tests/unit/sbol/test_domestication_service.py @@ -9,6 +9,7 @@ def test_domestication_service_returns_generated_plasmid_with_provenance() -> No source = sbol2.Document() target = sbol2.Document() part = sbol2.ComponentDefinition("https://example.org/part") + part.roles = ["https://example.org/role1", "https://example.org/role2"] source.addComponentDefinition(part) service = DomesticationService() @@ -31,4 +32,9 @@ def test_domestication_service_returns_generated_plasmid_with_provenance() -> No assert result.product.state == MaterialState.GENERATED assert result.product.metadata["source_part_identity"] == part.identity assert result.product.metadata["insert_identities"] == [part.identity] + assert result.product.roles == list(part.roles) + implementation_identity = result.product.metadata["implementation_identity"] + implementation = target.find(implementation_identity) + assert isinstance(implementation, sbol2.Implementation) + assert implementation.built == result.product.identity assert result.logs From 3fdcf3e0c9fa2f7a844af00eaa2fdc58bfa3d8b6 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Wed, 6 May 2026 00:04:27 -0600 Subject: [PATCH 56/93] Implement lvl2 assembly stage with route blocking promotion --- src/buildcompiler/stages/__init__.py | 3 +- src/buildcompiler/stages/assembly_lvl2.py | 261 ++++++++++++++++++++++ tests/unit/stages/test_assembly_lvl2.py | 191 ++++++++++++++++ 3 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 src/buildcompiler/stages/assembly_lvl2.py create mode 100644 tests/unit/stages/test_assembly_lvl2.py diff --git a/src/buildcompiler/stages/__init__.py b/src/buildcompiler/stages/__init__.py index d354b34..bacbb7d 100644 --- a/src/buildcompiler/stages/__init__.py +++ b/src/buildcompiler/stages/__init__.py @@ -1,6 +1,7 @@ """Stage exports.""" from .assembly_lvl1 import AssemblyLvl1Stage +from .assembly_lvl2 import AssemblyLvl2Stage from .domestication import DomesticationStage -__all__ = ["AssemblyLvl1Stage", "DomesticationStage"] +__all__ = ["AssemblyLvl1Stage", "AssemblyLvl2Stage", "DomesticationStage"] diff --git a/src/buildcompiler/stages/assembly_lvl2.py b/src/buildcompiler/stages/assembly_lvl2.py new file mode 100644 index 0000000..44d6b81 --- /dev/null +++ b/src/buildcompiler/stages/assembly_lvl2.py @@ -0,0 +1,261 @@ +"""Thin lvl2 assembly stage orchestration.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any + +import sbol2 + +from buildcompiler.adapters.pudu import assembly_route_to_pudu_json +from buildcompiler.api import BuildOptions +from buildcompiler.domain import ( + BuildRequest, + BuildStage, + MissingBuildInput, + StageResult, + StageStatus, +) +from buildcompiler.inventory import CompatibilitySelector, Inventory +from buildcompiler.inventory.compatibility import Lvl2Route +from buildcompiler.sbol import AssemblyJob, AssemblyService + + +class AssemblyLvl2Stage: + def __init__( + self, + *, + inventory: Inventory, + selector: CompatibilitySelector | None = None, + assembly_service: AssemblyService | None = None, + options: BuildOptions | None = None, + ) -> None: + self.inventory = inventory + self.options = options or BuildOptions() + self.selector = selector or CompatibilitySelector( + inventory, options=self.options + ) + self.assembly_service = assembly_service or AssemblyService() + + def run( + self, + request: BuildRequest, + *, + source_document: sbol2.Document, + target_document: sbol2.Document, + ) -> StageResult: + constraints = request.constraints or {} + module_definition = source_document.find(request.source_identity) + if not isinstance(module_definition, sbol2.ModuleDefinition): + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL2.value}", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.FAILED, + request_ids=[request.id], + logs=[ + f"Source identity is not a ModuleDefinition: {request.source_identity}" + ], + ) + + region_identities = self._extract_region_identities( + module_definition, constraints + ) + if not region_identities: + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL2.value}", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.FAILED, + request_ids=[request.id], + logs=["No engineered-region identities found for lvl2 assembly."], + ) + + route_selection = self.selector.select_lvl2_route( + request_id=request.id, + region_identities=region_identities, + constraints=constraints, + ) + route = route_selection.selected + artifacts = self._route_artifacts(route, route_selection.rejected) + if route is None: + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL2.value}", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.BLOCKED, + request_ids=[request.id], + protocol_artifacts=artifacts, + logs=[ + "No lvl2 route selected by CompatibilitySelector. Provide explicit region_order " + "or enable large-order search for large designs." + ], + ) + + missing_inputs: list[MissingBuildInput] = [] + for missing_identity in route.missing_region_identities: + missing_inputs.append( + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL2, + source_design_identity=request.source_identity, + missing_identity=missing_identity, + missing_display_id=missing_identity.rsplit("/", 1)[-1], + missing_kind="engineered_region", + required_stage=BuildStage.ASSEMBLY_LVL1, + reason="No compatible lvl1 engineered-region plasmid found in inventory.", + ) + ) + + if route.backbone is None: + missing_inputs.append( + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL2, + source_design_identity=request.source_identity, + missing_identity="backbone", + missing_display_id=None, + missing_kind="backbone", + required_stage="fatal", + reason="No compatible lvl2 backbone found in inventory.", + ) + ) + + restriction_enzyme_name = self.options.reagents.default_restriction_enzyme + restriction_enzyme = self.inventory.find_restriction_enzyme( + restriction_enzyme_name + ) + if restriction_enzyme is None: + missing_inputs.append( + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL2, + source_design_identity=request.source_identity, + missing_identity=restriction_enzyme_name, + missing_display_id=restriction_enzyme_name, + missing_kind="restriction_enzyme", + required_stage="fatal", + reason="Required restriction enzyme is missing from inventory.", + ) + ) + + ligase_name = self.options.reagents.default_ligase + ligase = self.inventory.find_ligase(ligase_name) + if ligase is None: + missing_inputs.append( + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL2, + source_design_identity=request.source_identity, + missing_identity=ligase_name, + missing_display_id=ligase_name, + missing_kind="ligase", + required_stage="fatal", + reason="Required ligase is missing from inventory.", + ) + ) + + if missing_inputs: + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL2.value}", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.BLOCKED, + request_ids=[request.id], + missing_inputs=missing_inputs, + protocol_artifacts=artifacts, + logs=[ + f"Blocked lvl2 assembly for {request.id}; missing {len(missing_inputs)} required input(s)." + ], + ) + + product_identity = ( + constraints.get("product_identity") or request.source_identity + ) + product_display_id = ( + constraints.get("product_display_id") + or request.source_display_id + or product_identity.rsplit("/", 1)[-1] + ) + assembly_result = self.assembly_service.run( + AssemblyJob( + stage=BuildStage.ASSEMBLY_LVL2, + product_identity=product_identity, + product_display_id=product_display_id, + part_plasmids=list(route.selected_lvl1_plasmids), + backbone=route.backbone, + restriction_enzyme=restriction_enzyme, + ligase=ligase, + source_document=source_document, + target_document=target_document, + ) + ) + + for product in assembly_result.products: + insert_identities = list(product.metadata.get("insert_identities", [])) + if request.source_identity not in insert_identities: + insert_identities.append(request.source_identity) + product.metadata["insert_identities"] = insert_identities + product.metadata.setdefault("source_stage", BuildStage.ASSEMBLY_LVL2.value) + self.inventory.add_generated_product(product) + + json_intermediate = assembly_route_to_pudu_json( + product_identity=product_identity, + part_plasmids=route.selected_lvl1_plasmids, + backbone=route.backbone, + restriction_enzyme=restriction_enzyme, + ) + + return StageResult( + id=f"{request.id}:{BuildStage.ASSEMBLY_LVL2.value}", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.SUCCESS, + request_ids=[request.id], + products=assembly_result.products, + sbol_document=assembly_result.stage_document, + json_intermediate=json_intermediate, + protocol_artifacts=artifacts, + logs=[ + f"Selected lvl2 route with {len(route.selected_lvl1_plasmids)} lvl1 plasmid(s).", + *assembly_result.logs, + ], + ) + + def _extract_region_identities( + self, module_definition: sbol2.ModuleDefinition, constraints: Mapping[str, Any] + ) -> list[str]: + constrained = constraints.get("region_order") + if constrained: + return list(constrained) + for key in ("engineered_region_identities", "region_identities"): + values = constraints.get(key) + if values: + return list(values) + + identities: list[str] = [] + for functional_component in module_definition.functionalComponents: + definition = functional_component.definition + if definition: + identities.append(definition) + return identities + + def _route_artifacts( + self, selected: Lvl2Route | None, rejected: tuple[Any, ...] + ) -> dict[str, Any]: + return { + "selected_route": self._route_to_dict(selected), + "rejected_routes": [self._route_to_dict(route) for route in rejected[:3]], + } + + def _route_to_dict(self, route: Lvl2Route | None) -> dict[str, Any] | None: + if route is None: + return None + return { + "region_order": list(route.region_order), + "selected_lvl1_plasmids": [ + p.identity for p in route.selected_lvl1_plasmids + ], + "missing_region_identities": list(route.missing_region_identities), + "score": { + "missing_required_products": route.score.missing_required_products, + "missing_domestications": route.score.missing_domestications, + "missing_lvl1_plasmids": route.score.missing_lvl1_plasmids, + "generated_or_planned_materials": route.score.generated_or_planned_materials, + "lower_material_state_penalty": route.score.lower_material_state_penalty, + "constraint_violations": route.score.constraint_violations, + "total_assemblies": route.score.total_assemblies, + "identity_tiebreak": list(route.score.identity_tiebreak), + }, + } diff --git a/tests/unit/stages/test_assembly_lvl2.py b/tests/unit/stages/test_assembly_lvl2.py new file mode 100644 index 0000000..281d317 --- /dev/null +++ b/tests/unit/stages/test_assembly_lvl2.py @@ -0,0 +1,191 @@ +import sbol2 + +from buildcompiler.api import BuildOptions +from buildcompiler.domain import ( + BuildRequest, + BuildStage, + DesignKind, + IndexedBackbone, + IndexedPlasmid, + IndexedReagent, + MaterialState, + StageStatus, +) +from buildcompiler.inventory import Inventory +from buildcompiler.sbol import AssemblySbolResult +from buildcompiler.stages import AssemblyLvl2Stage + + +class _FakeAssemblyService: + def __init__(self, products): + self.products = products + + def run(self, job): + return AssemblySbolResult( + products=self.products, + stage_document=job.target_document, + activity_identity="https://example.org/activity/lvl2", + logs=["fake-lvl2-assembly-service-ran"], + ) + + +def _module_doc(): + doc = sbol2.Document() + module = sbol2.ModuleDefinition("design_mod") + region1 = sbol2.ComponentDefinition("region1", sbol2.BIOPAX_DNA) + region2 = sbol2.ComponentDefinition("region2", sbol2.BIOPAX_DNA) + doc.addComponentDefinition(region1) + doc.addComponentDefinition(region2) + fc1 = module.functionalComponents.create("fc1") + fc1.definition = region1.identity + fc2 = module.functionalComponents.create("fc2") + fc2.definition = region2.identity + doc.addModuleDefinition(module) + return doc, module, [region1.identity, region2.identity] + + +def _request(source_identity, constraints=None): + return BuildRequest( + id="req-lvl2", + stage=BuildStage.ASSEMBLY_LVL2, + source_identity=source_identity, + source_display_id="design_mod", + source_kind=DesignKind.MODULE_DEFINITION, + constraints=constraints or {}, + ) + + +def _inventory( + region_identities=None, + *, + include_regions=True, + include_backbone=True, + include_reagents=True, +): + plasmids = [] + if include_regions and region_identities: + plasmids = [ + IndexedPlasmid( + identity="https://example.org/plasmids/lvl1-r1", + metadata={"insert_identities": [region_identities[0]]}, + state=MaterialState.ASSEMBLED, + ), + IndexedPlasmid( + identity="https://example.org/plasmids/lvl1-r2", + metadata={"insert_identities": [region_identities[1]]}, + state=MaterialState.ASSEMBLED, + ), + ] + backbones = [] + if include_backbone: + backbones = [ + IndexedBackbone( + identity="https://example.org/backbones/lvl2", + metadata={"stage": BuildStage.ASSEMBLY_LVL2.value}, + ) + ] + reagents = [] + if include_reagents: + reagents = [ + IndexedReagent( + identity="https://example.org/reagents/bsaI", + name="BsaI", + reagent_type="restriction_enzyme", + ), + IndexedReagent( + identity="https://example.org/reagents/ligase", + name="T4_DNA_ligase", + reagent_type="ligase", + ), + ] + return Inventory(plasmids=plasmids, backbones=backbones, reagents=reagents) + + +def test_assembly_lvl2_success_routes_and_indexes_generated_product(): + doc, module, regions = _module_doc() + inv = _inventory(regions) + generated = IndexedPlasmid( + identity="https://example.org/plasmids/lvl2-product", + state=MaterialState.GENERATED, + ) + stage = AssemblyLvl2Stage( + inventory=inv, assembly_service=_FakeAssemblyService([generated]) + ) + + result = stage.run( + _request(module.identity), source_document=doc, target_document=sbol2.Document() + ) + + assert result.status == StageStatus.SUCCESS + assert result.products and result.products[0].identity == generated.identity + assert ( + result.json_intermediate + and result.json_intermediate["Product"] == module.identity + ) + assert result.protocol_artifacts["selected_route"] is not None + assert inv.find_lvl1_region_plasmids(module.identity) + + +def test_assembly_lvl2_no_regions_failed(): + doc = sbol2.Document() + module = sbol2.ModuleDefinition("empty_mod") + doc.addModuleDefinition(module) + stage = AssemblyLvl2Stage(inventory=_inventory([], include_regions=False)) + + result = stage.run( + _request(module.identity), source_document=doc, target_document=sbol2.Document() + ) + + assert result.status == StageStatus.FAILED + + +def test_assembly_lvl2_missing_engineered_regions_promote_to_lvl1_blockers(): + doc, module, regions = _module_doc() + stage = AssemblyLvl2Stage(inventory=_inventory(regions, include_regions=False)) + + result = stage.run( + _request(module.identity), source_document=doc, target_document=sbol2.Document() + ) + + assert result.status == StageStatus.BLOCKED + engineered = [ + m for m in result.missing_inputs if m.missing_kind == "engineered_region" + ] + assert engineered + assert all(m.required_stage == BuildStage.ASSEMBLY_LVL1 for m in engineered) + + +def test_assembly_lvl2_large_order_requires_opt_in_without_explicit_order(): + options = BuildOptions() + options.planning.lvl2_search.max_exhaustive_region_count = 4 + options.planning.lvl2_search.allow_large_order_search = False + stage = AssemblyLvl2Stage( + inventory=_inventory([], include_regions=False), options=options + ) + constraints = {"region_identities": [f"https://example.org/r{i}" for i in range(5)]} + + doc, module, _ = _module_doc() + result = stage.run( + _request(module.identity, constraints=constraints), + source_document=doc, + target_document=sbol2.Document(), + ) + + assert result.status == StageStatus.BLOCKED + assert result.protocol_artifacts["selected_route"] is None + + +def test_assembly_lvl2_region_order_constraint_is_hard(): + doc, module, regions = _module_doc() + inv = _inventory(regions) + stage = AssemblyLvl2Stage(inventory=inv, assembly_service=_FakeAssemblyService([])) + + order = [regions[1], regions[0]] + result = stage.run( + _request(module.identity, constraints={"region_order": order}), + source_document=doc, + target_document=sbol2.Document(), + ) + + assert result.status == StageStatus.SUCCESS + assert result.protocol_artifacts["selected_route"]["region_order"] == order From a85ed1380fb4f519b11422d10bfc6afb1f71808b Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Wed, 6 May 2026 00:19:12 -0600 Subject: [PATCH 57/93] Fix lvl2 region_order handling to preserve requested region set --- src/buildcompiler/stages/assembly_lvl2.py | 3 --- tests/unit/stages/test_assembly_lvl2.py | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/buildcompiler/stages/assembly_lvl2.py b/src/buildcompiler/stages/assembly_lvl2.py index 44d6b81..3ea312a 100644 --- a/src/buildcompiler/stages/assembly_lvl2.py +++ b/src/buildcompiler/stages/assembly_lvl2.py @@ -216,9 +216,6 @@ def run( def _extract_region_identities( self, module_definition: sbol2.ModuleDefinition, constraints: Mapping[str, Any] ) -> list[str]: - constrained = constraints.get("region_order") - if constrained: - return list(constrained) for key in ("engineered_region_identities", "region_identities"): values = constraints.get(key) if values: diff --git a/tests/unit/stages/test_assembly_lvl2.py b/tests/unit/stages/test_assembly_lvl2.py index 281d317..bfaeab3 100644 --- a/tests/unit/stages/test_assembly_lvl2.py +++ b/tests/unit/stages/test_assembly_lvl2.py @@ -189,3 +189,19 @@ def test_assembly_lvl2_region_order_constraint_is_hard(): assert result.status == StageStatus.SUCCESS assert result.protocol_artifacts["selected_route"]["region_order"] == order + + +def test_assembly_lvl2_incomplete_region_order_blocks(): + doc, module, regions = _module_doc() + inv = _inventory(regions) + stage = AssemblyLvl2Stage(inventory=inv, assembly_service=_FakeAssemblyService([])) + + incomplete_order = [regions[0]] + result = stage.run( + _request(module.identity, constraints={"region_order": incomplete_order}), + source_document=doc, + target_document=sbol2.Document(), + ) + + assert result.status == StageStatus.BLOCKED + assert result.protocol_artifacts["selected_route"] is None From 06e8290bfccb9d60bf8b0d62e0074accb762cf7f Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Wed, 6 May 2026 00:19:52 -0600 Subject: [PATCH 58/93] Fallback to arbitrary lvl2 order when region_order cannot be satisfied --- src/buildcompiler/stages/assembly_lvl2.py | 26 ++++++++++++++++++----- tests/unit/stages/test_assembly_lvl2.py | 17 +++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/buildcompiler/stages/assembly_lvl2.py b/src/buildcompiler/stages/assembly_lvl2.py index 44d6b81..31d6eb6 100644 --- a/src/buildcompiler/stages/assembly_lvl2.py +++ b/src/buildcompiler/stages/assembly_lvl2.py @@ -74,6 +74,22 @@ def run( region_identities=region_identities, constraints=constraints, ) + warning_logs: list[str] = [] + if route_selection.selected is None and constraints.get("region_order"): + relaxed_constraints = { + key: value for key, value in constraints.items() if key != "region_order" + } + relaxed_selection = self.selector.select_lvl2_route( + request_id=request.id, + region_identities=region_identities, + constraints=relaxed_constraints, + ) + if relaxed_selection.selected is not None: + route_selection = relaxed_selection + warning_logs.append( + "Unable to satisfy region_order constraint; proceeding with an arbitrary compatible order." + ) + route = route_selection.selected artifacts = self._route_artifacts(route, route_selection.rejected) if route is None: @@ -85,7 +101,8 @@ def run( protocol_artifacts=artifacts, logs=[ "No lvl2 route selected by CompatibilitySelector. Provide explicit region_order " - "or enable large-order search for large designs." + "or enable large-order search for large designs.", + *warning_logs, ], ) @@ -157,7 +174,8 @@ def run( missing_inputs=missing_inputs, protocol_artifacts=artifacts, logs=[ - f"Blocked lvl2 assembly for {request.id}; missing {len(missing_inputs)} required input(s)." + *warning_logs, + f"Blocked lvl2 assembly for {request.id}; missing {len(missing_inputs)} required input(s).", ], ) @@ -208,6 +226,7 @@ def run( json_intermediate=json_intermediate, protocol_artifacts=artifacts, logs=[ + *warning_logs, f"Selected lvl2 route with {len(route.selected_lvl1_plasmids)} lvl1 plasmid(s).", *assembly_result.logs, ], @@ -216,9 +235,6 @@ def run( def _extract_region_identities( self, module_definition: sbol2.ModuleDefinition, constraints: Mapping[str, Any] ) -> list[str]: - constrained = constraints.get("region_order") - if constrained: - return list(constrained) for key in ("engineered_region_identities", "region_identities"): values = constraints.get(key) if values: diff --git a/tests/unit/stages/test_assembly_lvl2.py b/tests/unit/stages/test_assembly_lvl2.py index 281d317..17466c7 100644 --- a/tests/unit/stages/test_assembly_lvl2.py +++ b/tests/unit/stages/test_assembly_lvl2.py @@ -189,3 +189,20 @@ def test_assembly_lvl2_region_order_constraint_is_hard(): assert result.status == StageStatus.SUCCESS assert result.protocol_artifacts["selected_route"]["region_order"] == order + + +def test_assembly_lvl2_incomplete_region_order_falls_back_with_warning(): + doc, module, regions = _module_doc() + inv = _inventory(regions) + stage = AssemblyLvl2Stage(inventory=inv, assembly_service=_FakeAssemblyService([])) + + incomplete_order = [regions[0]] + result = stage.run( + _request(module.identity, constraints={"region_order": incomplete_order}), + source_document=doc, + target_document=sbol2.Document(), + ) + + assert result.status == StageStatus.SUCCESS + assert result.protocol_artifacts["selected_route"] is not None + assert any("Unable to satisfy region_order constraint" in log for log in result.logs) From 1a71a26eb24541a075c8f65c2ff6832c30597768 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Wed, 6 May 2026 09:34:33 -0600 Subject: [PATCH 59/93] Implement bounded full-build executor and wire compiler API --- src/buildcompiler/api/compiler.py | 68 +++-- src/buildcompiler/execution/__init__.py | 7 +- src/buildcompiler/execution/context.py | 20 ++ src/buildcompiler/execution/executor.py | 285 ++++++++++++++++++ src/buildcompiler/inventory/selector.py | 2 +- src/buildcompiler/planning/combinatorial.py | 2 +- .../planning/full_build_planner.py | 2 +- src/buildcompiler/stages/assembly_lvl1.py | 2 +- src/buildcompiler/stages/assembly_lvl2.py | 2 +- src/buildcompiler/stages/domestication.py | 2 +- tests/unit/api/test_compiler_api.py | 8 +- tests/unit/execution/test_executor.py | 147 +++++++++ 12 files changed, 502 insertions(+), 45 deletions(-) create mode 100644 src/buildcompiler/execution/context.py create mode 100644 src/buildcompiler/execution/executor.py create mode 100644 tests/unit/execution/test_executor.py diff --git a/src/buildcompiler/api/compiler.py b/src/buildcompiler/api/compiler.py index 2823293..2a82db0 100644 --- a/src/buildcompiler/api/compiler.py +++ b/src/buildcompiler/api/compiler.py @@ -5,13 +5,13 @@ from dataclasses import dataclass, field from typing import Any +from buildcompiler.planning import FullBuildPlanner + from .options import BuildOptions @dataclass class BuildCompiler: - """Lightweight dependency-injected compiler facade.""" - inventory: Any = None sbol_document: Any = None planner: Any = None @@ -30,43 +30,42 @@ def from_synbiohub( options: BuildOptions | None = None, **kwargs: Any, ) -> "BuildCompiler": - """Factory boundary reserved for future SynBioHub loading/indexing.""" if collections: raise NotImplementedError( - "Automatic SynBioHub collection loading/indexing is not implemented yet. " - "Inject inventory dependencies directly for now." + "Automatic SynBioHub collection loading/indexing is not implemented yet. Inject inventory dependencies directly for now." ) - - return cls( - sbol_document=sbol_doc, - options=options or BuildOptions(), - **kwargs, - ) + return cls(sbol_document=sbol_doc, options=options or BuildOptions(), **kwargs) def plan(self, abstract_designs: Any, options: BuildOptions | None = None) -> Any: - """Plan a build request via injected planner (placeholder in skeleton).""" - if self.planner is None: - raise NotImplementedError( - "Build planning is not implemented in the API skeleton. " - "Inject a planner dependency to use BuildCompiler.plan()." - ) - effective_options = options or self.options - return self.planner.plan(abstract_designs, options=effective_options) + planner = self.planner or FullBuildPlanner(options=effective_options) + return planner.plan(abstract_designs, options=effective_options) def execute(self, plan: Any, options: BuildOptions | None = None) -> Any: - """Execute a build plan via injected executor (placeholder in skeleton).""" - if self.executor is None: - raise NotImplementedError( - "Build execution is not implemented in the API skeleton. " - "Inject an executor dependency to use BuildCompiler.execute()." - ) - effective_options = options or self.options - return self.executor.execute(plan, options=effective_options) + executor = self.executor + if executor is None: + if self.inventory is None: + raise ValueError( + "BuildCompiler.execute requires an inventory when no executor is injected." + ) + if self.sbol_document is None: + raise ValueError( + "BuildCompiler.execute requires an sbol_document when no executor is injected." + ) + from buildcompiler.execution import FullBuildExecutor + + executor = FullBuildExecutor.from_dependencies( + inventory=self.inventory, + sbol_document=self.sbol_document, + options=effective_options, + adapters=self.adapters, + ) + return executor.execute(plan, options=effective_options) - def full_build(self, abstract_designs: Any, options: BuildOptions | None = None) -> Any: - """Convenience skeleton method: plan then execute.""" + def full_build( + self, abstract_designs: Any, options: BuildOptions | None = None + ) -> Any: plan = self.plan(abstract_designs, options=options) return self.execute(plan, options=options) @@ -86,10 +85,13 @@ def full_build( sbol_doc: Any = None, **kwargs: Any, ) -> Any: - """Module-level full-build entry point for the public API skeleton.""" compiler_options = options or BuildOptions() - - if collections is not None or sbh_registry is not None or auth_token is not None or sbol_doc is not None: + if ( + collections is not None + or sbh_registry is not None + or auth_token is not None + or sbol_doc is not None + ): compiler = BuildCompiler.from_synbiohub( collections=collections, sbh_registry=sbh_registry, @@ -105,11 +107,11 @@ def full_build( else: compiler = BuildCompiler( inventory=inventory, + sbol_document=sbol_document, planner=planner, executor=executor, adapters=adapters, options=compiler_options, **kwargs, ) - return compiler.full_build(abstract_designs, options=compiler_options) diff --git a/src/buildcompiler/execution/__init__.py b/src/buildcompiler/execution/__init__.py index 5e19692..a8357fa 100644 --- a/src/buildcompiler/execution/__init__.py +++ b/src/buildcompiler/execution/__init__.py @@ -1 +1,6 @@ -"""Package scaffolding for clean architecture.""" +"""Execution package exports.""" + +from .context import BuildContext +from .executor import FullBuildExecutor + +__all__ = ["BuildContext", "FullBuildExecutor"] diff --git a/src/buildcompiler/execution/context.py b/src/buildcompiler/execution/context.py new file mode 100644 index 0000000..9c17cdf --- /dev/null +++ b/src/buildcompiler/execution/context.py @@ -0,0 +1,20 @@ +"""Execution context contract for full-build orchestration.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from buildcompiler.inventory import Inventory +from buildcompiler.sbol import SbolResolver + + +@dataclass +class BuildContext: + sbol: SbolResolver + inventory: Inventory + build_document: Any + options: Any + adapters: Any = None + graph: Any = None + logger: Any = None diff --git a/src/buildcompiler/execution/executor.py b/src/buildcompiler/execution/executor.py new file mode 100644 index 0000000..cad9c4f --- /dev/null +++ b/src/buildcompiler/execution/executor.py @@ -0,0 +1,285 @@ +"""Bounded fixed-point full-build executor.""" + +from __future__ import annotations + +import hashlib +from collections import OrderedDict +from typing import Any + +from buildcompiler.api.options import BuildOptions +from buildcompiler.domain import ( + BuildRequest, + BuildStage, + BuildStatus, + DesignKind, + FullBuildResult, + MissingBuildInput, + StageResult, + StageStatus, +) +from buildcompiler.execution.context import BuildContext +from buildcompiler.inventory import Inventory +from buildcompiler.planning import BuildPlan +from buildcompiler.sbol import SbolResolver +from buildcompiler.stages import ( + AssemblyLvl1Stage, + AssemblyLvl2Stage, + DomesticationStage, +) + + +class FullBuildExecutor: + def __init__( + self, + *, + context: BuildContext, + lvl2_stage: Any | None = None, + lvl1_stage: Any | None = None, + domestication_stage: Any | None = None, + transformation_stage: Any | None = None, + plating_stage: Any | None = None, + ) -> None: + self.context = context + options = context.options + self.lvl2_stage = lvl2_stage or AssemblyLvl2Stage( + inventory=context.inventory, options=options + ) + self.lvl1_stage = lvl1_stage or AssemblyLvl1Stage( + inventory=context.inventory, options=options + ) + self.domestication_stage = domestication_stage or DomesticationStage( + inventory=context.inventory, options=options + ) + self.transformation_stage = transformation_stage + self.plating_stage = plating_stage + + @classmethod + def from_dependencies( + cls, + *, + inventory: Inventory, + sbol_document: Any, + options: BuildOptions, + adapters: Any = None, + graph: Any = None, + logger: Any = None, + **stage_overrides: Any, + ) -> "FullBuildExecutor": + resolver = SbolResolver(sbol_document) + return cls( + context=BuildContext( + sbol=resolver, + inventory=inventory, + build_document=sbol_document, + options=options, + adapters=adapters, + graph=graph, + logger=logger, + ), + **stage_overrides, + ) + + def execute( + self, plan: BuildPlan, *, options: BuildOptions | None = None + ) -> FullBuildResult: + if options is not None: + self.context.options = options + + pending = { + BuildStage.ASSEMBLY_LVL2: OrderedDict( + (r.id, r) for r in plan.lvl2_requests + ), + BuildStage.ASSEMBLY_LVL1: OrderedDict( + (r.id, r) for r in plan.lvl1_requests + ), + BuildStage.DOMESTICATION: OrderedDict( + (r.id, r) for r in plan.domestication_requests + ), + } + completed: set[str] = set() + stage_results: list[StageResult] = [] + final_products: dict[str, Any] = {} + missing_by_key: dict[tuple, MissingBuildInput] = {} + approvals: dict[str, Any] = {} + warnings: list[Any] = list(plan.warnings) + seen_products: set[str] = set() + transformed: set[str] = set() + plated: set[str] = set() + + for _ in range(self.context.options.execution.max_iterations): + progress = False + for stage in ( + BuildStage.ASSEMBLY_LVL2, + BuildStage.ASSEMBLY_LVL1, + BuildStage.DOMESTICATION, + ): + runner = { + BuildStage.ASSEMBLY_LVL2: self.lvl2_stage, + BuildStage.ASSEMBLY_LVL1: self.lvl1_stage, + BuildStage.DOMESTICATION: self.domestication_stage, + }[stage] + for request in list(pending[stage].values()): + result = self._run_stage(runner, request) + stage_results.append(result) + warnings.extend(result.warnings) + for approval in result.required_approvals: + approvals[str(approval)] = approval + if result.status in ( + StageStatus.SUCCESS, + StageStatus.PARTIAL_SUCCESS, + ): + if request.id not in completed: + completed.add(request.id) + progress = True + pending[stage].pop(request.id, None) + progress = ( + self._index_products(result, final_products, seen_products) + or progress + ) + progress = ( + self._chain( + result.products, stage_results, transformed, plated + ) + or progress + ) + else: + for missing in result.missing_inputs: + missing_by_key[self._missing_key(missing)] = missing + promoted = self._promote(request, missing) + if ( + promoted is not None + and promoted.id not in pending[promoted.stage] + and promoted.id not in completed + ): + pending[promoted.stage][promoted.id] = promoted + progress = True + + if not any(pending[s] for s in pending): + break + if not progress: + break + + unresolved = [ + m + for m in missing_by_key.values() + if self._promote(None, m) is None + or self._promote(None, m).id not in completed + ] + products = list(final_products.values()) + status = ( + BuildStatus.SUCCESS + if (not unresolved and not any(pending[s] for s in pending)) + else (BuildStatus.PARTIAL_SUCCESS if products else BuildStatus.FAILED) + ) + return FullBuildResult( + status=status, + plan=plan, + build_document=self.context.build_document, + stage_results=stage_results, + graph=self.context.graph, + final_products=products, + missing_inputs=unresolved, + required_approvals=list(approvals.values()), + warnings=warnings, + ) + + def _run_stage(self, stage: Any, request: BuildRequest) -> StageResult: + source_document = ( + getattr(self.context.sbol, "document", None) or self.context.build_document + ) + try: + return stage.run( + request, + source_document=source_document, + target_document=self.context.build_document, + ) + except Exception: + if not self.context.options.execution.continue_on_error: + raise + return StageResult( + id=f"{request.id}:{request.stage.value}", + stage=request.stage, + status=StageStatus.FAILED, + request_ids=[request.id], + logs=["Unexpected execution error."], + ) + + def _index_products( + self, + result: StageResult, + final_products: dict[str, Any], + seen_products: set[str], + ) -> bool: + progress = False + for product in result.products: + if product.identity not in seen_products: + seen_products.add(product.identity) + self.context.inventory.add_generated_product(product) + final_products[product.identity] = product + progress = True + return progress + + def _promote( + self, request: BuildRequest | None, missing: MissingBuildInput + ) -> BuildRequest | None: + if missing.required_stage not in ( + BuildStage.ASSEMBLY_LVL1, + BuildStage.DOMESTICATION, + ): + return None + prefix = ( + "lvl1" + if missing.required_stage == BuildStage.ASSEMBLY_LVL1 + else "domestication" + ) + digest = hashlib.sha1(missing.missing_identity.encode()).hexdigest()[:12] + return BuildRequest( + id=f"promoted:{prefix}:{digest}", + stage=missing.required_stage, + source_identity=missing.missing_identity, + source_display_id=missing.missing_display_id, + source_kind=DesignKind.COMPONENT_DEFINITION, + parent_group=request.id if request else missing.source_design_identity, + constraints={ + "promoted_from_stage": missing.source_stage.value, + "promoted_from_design_identity": missing.source_design_identity, + "candidates_tried": list(missing.candidates_tried), + }, + ) + + def _missing_key(self, missing: MissingBuildInput) -> tuple: + return ( + missing.source_stage.value, + missing.source_design_identity, + missing.missing_identity, + missing.missing_kind, + str(missing.required_stage), + missing.reason, + ) + + def _chain( + self, + products: list[Any], + stage_results: list[StageResult], + transformed: set[str], + plated: set[str], + ) -> bool: + progress = False + if self.transformation_stage is None: + return False + for product in products: + if product.identity in transformed: + continue + transformed.add(product.identity) + t_result = self.transformation_stage.run(product) + stage_results.append(t_result) + progress = True + if self.plating_stage is None: + continue + for out in t_result.products: + if out.identity in plated: + continue + plated.add(out.identity) + stage_results.append(self.plating_stage.run(out)) + progress = True + return progress diff --git a/src/buildcompiler/inventory/selector.py b/src/buildcompiler/inventory/selector.py index 07ddd31..1b712e9 100644 --- a/src/buildcompiler/inventory/selector.py +++ b/src/buildcompiler/inventory/selector.py @@ -6,7 +6,7 @@ from itertools import permutations from typing import Any -from buildcompiler.api import BuildOptions +from buildcompiler.api.options import BuildOptions from buildcompiler.domain import BuildStage, MaterialState from buildcompiler.inventory.compatibility import Lvl1Route, Lvl2Route, RouteScore, RouteSelection from buildcompiler.inventory.inventory import Inventory diff --git a/src/buildcompiler/planning/combinatorial.py b/src/buildcompiler/planning/combinatorial.py index a94a8bc..101ec02 100644 --- a/src/buildcompiler/planning/combinatorial.py +++ b/src/buildcompiler/planning/combinatorial.py @@ -1,7 +1,7 @@ from __future__ import annotations import itertools import sbol2 -from buildcompiler.api import BuildOptions +from buildcompiler.api.options import BuildOptions from buildcompiler.domain import BuildRequest, BuildStage, BuildWarning, DesignKind from buildcompiler.planning.classifier import request_id_for from buildcompiler.planning.models import UnsupportedPlanningRecord diff --git a/src/buildcompiler/planning/full_build_planner.py b/src/buildcompiler/planning/full_build_planner.py index a4189fa..8814fbf 100644 --- a/src/buildcompiler/planning/full_build_planner.py +++ b/src/buildcompiler/planning/full_build_planner.py @@ -6,7 +6,7 @@ import sbol2 -from buildcompiler.api import BuildOptions +from buildcompiler.api.options import BuildOptions from buildcompiler.planning.classifier import classify_non_combinatorial from buildcompiler.planning.combinatorial import expand_combinatorial_derivation from buildcompiler.planning.models import BuildPlan, UnsupportedPlanningRecord diff --git a/src/buildcompiler/stages/assembly_lvl1.py b/src/buildcompiler/stages/assembly_lvl1.py index 3332eb7..04ada99 100644 --- a/src/buildcompiler/stages/assembly_lvl1.py +++ b/src/buildcompiler/stages/assembly_lvl1.py @@ -8,7 +8,7 @@ import sbol2 from buildcompiler.adapters.pudu import assembly_route_to_pudu_json -from buildcompiler.api import BuildOptions +from buildcompiler.api.options import BuildOptions from buildcompiler.domain import ( BuildRequest, BuildStage, diff --git a/src/buildcompiler/stages/assembly_lvl2.py b/src/buildcompiler/stages/assembly_lvl2.py index 31d6eb6..cdc0050 100644 --- a/src/buildcompiler/stages/assembly_lvl2.py +++ b/src/buildcompiler/stages/assembly_lvl2.py @@ -8,7 +8,7 @@ import sbol2 from buildcompiler.adapters.pudu import assembly_route_to_pudu_json -from buildcompiler.api import BuildOptions +from buildcompiler.api.options import BuildOptions from buildcompiler.domain import ( BuildRequest, BuildStage, diff --git a/src/buildcompiler/stages/domestication.py b/src/buildcompiler/stages/domestication.py index 62e9618..5e729ba 100644 --- a/src/buildcompiler/stages/domestication.py +++ b/src/buildcompiler/stages/domestication.py @@ -4,7 +4,7 @@ import sbol2 -from buildcompiler.api import BuildOptions, ProtocolMode +from buildcompiler.api.options import BuildOptions, ProtocolMode from buildcompiler.domain import ( ApprovalStatus, BuildRequest, diff --git a/tests/unit/api/test_compiler_api.py b/tests/unit/api/test_compiler_api.py index 23a34d6..648d61e 100644 --- a/tests/unit/api/test_compiler_api.py +++ b/tests/unit/api/test_compiler_api.py @@ -80,12 +80,10 @@ def test_from_synbiohub_raises_when_collection_loading_is_requested(): BuildCompiler.from_synbiohub(collections=["https://example.org/collection"]) -def test_plan_execute_raise_without_injected_dependencies(): +def test_execute_raises_clear_error_without_dependencies(): compiler = BuildCompiler() - with pytest.raises(NotImplementedError, match="planning"): - compiler.plan({"x": 1}) - - with pytest.raises(NotImplementedError, match="execution"): + compiler.plan([object()]) + with pytest.raises(ValueError, match="inventory"): compiler.execute({"plan": 1}) diff --git a/tests/unit/execution/test_executor.py b/tests/unit/execution/test_executor.py new file mode 100644 index 0000000..f439dc3 --- /dev/null +++ b/tests/unit/execution/test_executor.py @@ -0,0 +1,147 @@ +from buildcompiler.api import BuildOptions +from buildcompiler.domain import ( + BuildRequest, + BuildStage, + BuildStatus, + DesignKind, + IndexedPlasmid, + MaterialState, + MissingBuildInput, + StageResult, + StageStatus, +) +from buildcompiler.execution import BuildContext, FullBuildExecutor +from buildcompiler.inventory import Inventory +from buildcompiler.planning import BuildPlan +from buildcompiler.sbol import SbolResolver + + +class FakeStage: + def __init__(self, fn): + self.fn = fn + self.calls = [] + + def run(self, request, *, source_document, target_document): + self.calls.append(request.id) + return self.fn(request) + + +def plasmid(identity): + return IndexedPlasmid( + identity=identity, + display_id=identity.split("/")[-1], + state=MaterialState.GENERATED, + ) + + +def test_imports_smoke(): + assert BuildContext + assert FullBuildExecutor + + +def test_promote_and_retry_flow(): + lvl2_count = {"n": 0} + + def lvl2(request): + lvl2_count["n"] += 1 + if lvl2_count["n"] == 1: + return StageResult( + id="r", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.BLOCKED, + request_ids=[request.id], + missing_inputs=[ + MissingBuildInput( + BuildStage.ASSEMBLY_LVL2, + request.source_identity, + "https://x/region", + "region", + "engineered_region", + BuildStage.ASSEMBLY_LVL1, + "missing", + ) + ], + ) + return StageResult( + id="r", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.SUCCESS, + request_ids=[request.id], + products=[plasmid("https://x/lvl2")], + ) + + def lvl1(request): + return StageResult( + id="r1", + stage=BuildStage.ASSEMBLY_LVL1, + status=StageStatus.SUCCESS, + request_ids=[request.id], + products=[plasmid("https://x/region")], + ) + + ctx = BuildContext( + sbol=SbolResolver(__import__("sbol2").Document()), + inventory=Inventory(), + build_document=__import__("sbol2").Document(), + options=BuildOptions(), + ) + ex = FullBuildExecutor( + context=ctx, + lvl2_stage=FakeStage(lvl2), + lvl1_stage=FakeStage(lvl1), + domestication_stage=FakeStage( + lambda r: StageResult( + id="d", + stage=BuildStage.DOMESTICATION, + status=StageStatus.BLOCKED, + request_ids=[r.id], + ) + ), + ) + plan = BuildPlan( + lvl2_requests=[ + BuildRequest( + id="req-l2", + stage=BuildStage.ASSEMBLY_LVL2, + source_identity="https://x/mod", + source_display_id="mod", + source_kind=DesignKind.MODULE_DEFINITION, + ) + ] + ) + result = ex.execute(plan) + assert result.status == BuildStatus.SUCCESS + assert any(r.stage == BuildStage.ASSEMBLY_LVL1 for r in result.stage_results) + assert len(result.final_products) == 2 + + +def test_max_iteration_stops(): + options = BuildOptions() + options.execution.max_iterations = 2 + blocked = FakeStage( + lambda r: StageResult( + id="b", stage=r.stage, status=StageStatus.BLOCKED, request_ids=[r.id] + ) + ) + ctx = BuildContext( + sbol=SbolResolver(__import__("sbol2").Document()), + inventory=Inventory(), + build_document=__import__("sbol2").Document(), + options=options, + ) + ex = FullBuildExecutor( + context=ctx, lvl2_stage=blocked, lvl1_stage=blocked, domestication_stage=blocked + ) + plan = BuildPlan( + lvl2_requests=[ + BuildRequest( + id="req", + stage=BuildStage.ASSEMBLY_LVL2, + source_identity="x", + source_display_id="x", + source_kind=DesignKind.MODULE_DEFINITION, + ) + ] + ) + result = ex.execute(plan) + assert result.status == BuildStatus.FAILED From 672f78012cb7108f760e883d1dd681a384c02db1 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Wed, 6 May 2026 11:04:53 -0600 Subject: [PATCH 60/93] Add compiler-only protocol adapters and simulation boundary --- src/buildcompiler/adapters/__init__.py | 6 +- .../adapters/opentrons/__init__.py | 14 ++++- .../adapters/opentrons/simulation.py | 44 +++++++++++++++ src/buildcompiler/adapters/protocols.py | 55 +++++++++++++++++++ src/buildcompiler/adapters/pudu/__init__.py | 13 ++++- .../adapters/pudu/plating_json.py | 22 ++++++++ .../adapters/pudu/transformation_json.py | 53 ++++++++++++++++++ .../opentrons/test_simulation_boundary.py | 38 +++++++++++++ tests/unit/adapters/pudu/test_plating_json.py | 19 +++++++ .../adapters/pudu/test_transformation_json.py | 38 +++++++++++++ .../unit/adapters/test_protocol_boundaries.py | 38 +++++++++++++ tests/unit/test_core_imports.py | 21 +++++++ 12 files changed, 358 insertions(+), 3 deletions(-) create mode 100644 src/buildcompiler/adapters/opentrons/simulation.py create mode 100644 src/buildcompiler/adapters/protocols.py create mode 100644 src/buildcompiler/adapters/pudu/plating_json.py create mode 100644 src/buildcompiler/adapters/pudu/transformation_json.py create mode 100644 tests/unit/adapters/opentrons/test_simulation_boundary.py create mode 100644 tests/unit/adapters/pudu/test_plating_json.py create mode 100644 tests/unit/adapters/pudu/test_transformation_json.py create mode 100644 tests/unit/adapters/test_protocol_boundaries.py create mode 100644 tests/unit/test_core_imports.py diff --git a/src/buildcompiler/adapters/__init__.py b/src/buildcompiler/adapters/__init__.py index 5e19692..3390756 100644 --- a/src/buildcompiler/adapters/__init__.py +++ b/src/buildcompiler/adapters/__init__.py @@ -1 +1,5 @@ -"""Package scaffolding for clean architecture.""" +"""Adapter package exports without optional dependency side effects.""" + +from .protocols import ProtocolArtifact, maybe_write_protocol_artifacts + +__all__ = ["ProtocolArtifact", "maybe_write_protocol_artifacts"] diff --git a/src/buildcompiler/adapters/opentrons/__init__.py b/src/buildcompiler/adapters/opentrons/__init__.py index 5e19692..48db553 100644 --- a/src/buildcompiler/adapters/opentrons/__init__.py +++ b/src/buildcompiler/adapters/opentrons/__init__.py @@ -1 +1,13 @@ -"""Package scaffolding for clean architecture.""" +"""Optional Opentrons adapter exports.""" + +from .simulation import ( + OpentronsSimulationAdapter, + OptionalAutomationDependencyError, + SimulationResult, +) + +__all__ = [ + "OpentronsSimulationAdapter", + "OptionalAutomationDependencyError", + "SimulationResult", +] diff --git a/src/buildcompiler/adapters/opentrons/simulation.py b/src/buildcompiler/adapters/opentrons/simulation.py new file mode 100644 index 0000000..a8c8e17 --- /dev/null +++ b/src/buildcompiler/adapters/opentrons/simulation.py @@ -0,0 +1,44 @@ +"""Optional Opentrons simulation boundary adapter.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path + +from buildcompiler.api import ProtocolOptions + + +class OptionalAutomationDependencyError(ImportError): + """Raised when optional automation dependency is unavailable.""" + + +@dataclass +class SimulationResult: + ran: bool + logs: list[str] = field(default_factory=list) + metadata: dict[str, object] = field(default_factory=dict) + + +class OpentronsSimulationAdapter: + def simulate( + self, protocol_source: str | Path, *, options: ProtocolOptions + ) -> SimulationResult: + if not options.simulate: + return SimulationResult( + ran=False, + logs=["Simulation skipped: ProtocolOptions.simulate is False."], + metadata={"protocol_source": str(protocol_source)}, + ) + + try: + __import__("opentrons") + except ImportError as exc: + raise OptionalAutomationDependencyError( + "Install synbio-buildcompiler[automation] to use Opentrons simulation." + ) from exc + + return SimulationResult( + ran=True, + logs=["Simulation dependency check passed."], + metadata={"protocol_source": str(protocol_source)}, + ) diff --git a/src/buildcompiler/adapters/protocols.py b/src/buildcompiler/adapters/protocols.py new file mode 100644 index 0000000..92c8e7b --- /dev/null +++ b/src/buildcompiler/adapters/protocols.py @@ -0,0 +1,55 @@ +"""Protocol artifact boundaries for optional file output.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass, field +from pathlib import Path + +from buildcompiler.api import ProtocolMode, ProtocolOptions + + +@dataclass +class ProtocolArtifact: + kind: str + path: Path | None = None + content: str | dict[str, object] | list[dict[str, object]] | None = None + metadata: dict[str, object] = field(default_factory=dict) + + +def _artifact_filename(*, basename: str, kind: str) -> str: + safe_kind = kind.replace(" ", "_").lower() + return f"{basename}_{safe_kind}.json" + + +def maybe_write_protocol_artifacts( + *, + payloads: dict[str, object], + options: ProtocolOptions, + basename: str = "buildcompiler_protocol", +) -> dict[str, ProtocolArtifact]: + """Return in-memory protocol payloads and optionally write them to disk.""" + + should_write = ( + options.mode in {ProtocolMode.MANUAL, ProtocolMode.AUTOMATED} + and options.results_dir is not None + ) + output_dir = Path(options.results_dir) if should_write else None + if output_dir is not None: + output_dir.mkdir(parents=True, exist_ok=True) + + artifacts: dict[str, ProtocolArtifact] = {} + for kind, payload in payloads.items(): + artifact = ProtocolArtifact(kind=kind, content=payload) + if output_dir is not None: + path = output_dir / _artifact_filename(basename=basename, kind=kind) + path.write_text( + json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8" + ) + artifact.path = path + artifact.metadata["written"] = True + else: + artifact.metadata["written"] = False + artifacts[kind] = artifact + + return artifacts diff --git a/src/buildcompiler/adapters/pudu/__init__.py b/src/buildcompiler/adapters/pudu/__init__.py index a31fbde..50838bf 100644 --- a/src/buildcompiler/adapters/pudu/__init__.py +++ b/src/buildcompiler/adapters/pudu/__init__.py @@ -1,5 +1,16 @@ """PUDU adapter exports.""" from .assembly_json import assembly_route_to_pudu_json, assembly_routes_to_pudu_json +from .plating_json import plating_to_pudu_json +from .transformation_json import ( + transformation_to_pudu_json, + transformations_to_pudu_json, +) -__all__ = ["assembly_route_to_pudu_json", "assembly_routes_to_pudu_json"] +__all__ = [ + "assembly_route_to_pudu_json", + "assembly_routes_to_pudu_json", + "transformation_to_pudu_json", + "transformations_to_pudu_json", + "plating_to_pudu_json", +] diff --git a/src/buildcompiler/adapters/pudu/plating_json.py b/src/buildcompiler/adapters/pudu/plating_json.py new file mode 100644 index 0000000..3648e20 --- /dev/null +++ b/src/buildcompiler/adapters/pudu/plating_json.py @@ -0,0 +1,22 @@ +"""In-memory adapter for compiler-level PUDU plating JSON payloads.""" + +from collections import OrderedDict +from collections.abc import Mapping +from typing import Any + + +def plating_to_pudu_json( + *, + bacterium_locations: Mapping[str, str], + advanced_parameters: Mapping[str, object] | None = None, +) -> dict[str, object]: + """Adapt plating records into deterministic legacy-compatible PUDU JSON keys.""" + + stable_locations = OrderedDict( + sorted(bacterium_locations.items(), key=lambda kv: kv[0]) + ) + payload: dict[str, Any] = { + "bacterium_locations": dict(stable_locations), + "advanced_parameters": dict(advanced_parameters or {}), + } + return payload diff --git a/src/buildcompiler/adapters/pudu/transformation_json.py b/src/buildcompiler/adapters/pudu/transformation_json.py new file mode 100644 index 0000000..207a561 --- /dev/null +++ b/src/buildcompiler/adapters/pudu/transformation_json.py @@ -0,0 +1,53 @@ +"""In-memory adapter for compiler-level PUDU transformation JSON payloads.""" + +from collections.abc import Sequence + +from buildcompiler.domain import IndexedPlasmid + + +def _stable_identifier(identity: str, display_id: str | None) -> str: + return identity or display_id or "" + + +def _plasmid_identifier(plasmid: IndexedPlasmid | str) -> str: + if isinstance(plasmid, str): + return plasmid + return _stable_identifier(identity=plasmid.identity, display_id=plasmid.display_id) + + +def transformation_to_pudu_json( + *, + strain_identity: str, + chassis_identity: str, + plasmids: Sequence[IndexedPlasmid | str], +) -> dict[str, object]: + """Adapt a transformation record into legacy-compatible PUDU JSON keys.""" + + return { + "Strain": strain_identity, + "Chassis": chassis_identity, + "Plasmids": [_plasmid_identifier(plasmid) for plasmid in plasmids], + } + + +def transformations_to_pudu_json( + *, + strain_identities: Sequence[str], + chassis_identities: Sequence[str], + plasmid_sets: Sequence[Sequence[IndexedPlasmid | str]], +) -> list[dict[str, object]]: + """Batch helper for deterministic in-memory transformation JSON payloads.""" + + return [ + transformation_to_pudu_json( + strain_identity=strain_identity, + chassis_identity=chassis_identity, + plasmids=plasmids, + ) + for strain_identity, chassis_identity, plasmids in zip( + strain_identities, + chassis_identities, + plasmid_sets, + strict=True, + ) + ] diff --git a/tests/unit/adapters/opentrons/test_simulation_boundary.py b/tests/unit/adapters/opentrons/test_simulation_boundary.py new file mode 100644 index 0000000..53fbf2c --- /dev/null +++ b/tests/unit/adapters/opentrons/test_simulation_boundary.py @@ -0,0 +1,38 @@ +import builtins +import sys + +import pytest + +from buildcompiler.adapters.opentrons import ( + OpentronsSimulationAdapter, + OptionalAutomationDependencyError, +) +from buildcompiler.api import ProtocolOptions + + +def test_opentrons_import_is_lazy(): + assert "opentrons" not in sys.modules + + +def test_simulate_false_does_not_import_opentrons(): + adapter = OpentronsSimulationAdapter() + + result = adapter.simulate("protocol.py", options=ProtocolOptions(simulate=False)) + + assert result.ran is False + assert "opentrons" not in sys.modules + + +def test_simulate_true_missing_dependency_raises(monkeypatch): + adapter = OpentronsSimulationAdapter() + + real_import = builtins.__import__ + + def fake_import(name, *args, **kwargs): + if name == "opentrons": + raise ImportError("forced missing dependency") + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", fake_import) + with pytest.raises(OptionalAutomationDependencyError): + adapter.simulate("protocol.py", options=ProtocolOptions(simulate=True)) diff --git a/tests/unit/adapters/pudu/test_plating_json.py b/tests/unit/adapters/pudu/test_plating_json.py new file mode 100644 index 0000000..aa075e5 --- /dev/null +++ b/tests/unit/adapters/pudu/test_plating_json.py @@ -0,0 +1,19 @@ +from buildcompiler.adapters.pudu import plating_to_pudu_json + + +def test_plating_to_pudu_json_shape_and_values(): + payload = plating_to_pudu_json( + bacterium_locations={"strain_b": "B2", "strain_a": "A1"}, + advanced_parameters={"replicates": 2}, + ) + + assert payload == { + "bacterium_locations": {"strain_a": "A1", "strain_b": "B2"}, + "advanced_parameters": {"replicates": 2}, + } + + +def test_plating_to_pudu_json_defaults_advanced_parameters(): + payload = plating_to_pudu_json(bacterium_locations={"strain_a": "A1"}) + + assert payload["advanced_parameters"] == {} diff --git a/tests/unit/adapters/pudu/test_transformation_json.py b/tests/unit/adapters/pudu/test_transformation_json.py new file mode 100644 index 0000000..d18f7ab --- /dev/null +++ b/tests/unit/adapters/pudu/test_transformation_json.py @@ -0,0 +1,38 @@ +from buildcompiler.adapters.pudu import ( + transformation_to_pudu_json, + transformations_to_pudu_json, +) +from buildcompiler.domain import IndexedPlasmid + + +def test_transformation_to_pudu_json_shape_and_values(): + payload = transformation_to_pudu_json( + strain_identity="https://example.org/strain/s1", + chassis_identity="https://example.org/chassis/c1", + plasmids=[ + IndexedPlasmid(identity="https://example.org/plasmids/p1"), + "https://example.org/plasmids/p2", + ], + ) + + assert payload == { + "Strain": "https://example.org/strain/s1", + "Chassis": "https://example.org/chassis/c1", + "Plasmids": [ + "https://example.org/plasmids/p1", + "https://example.org/plasmids/p2", + ], + } + + +def test_transformations_to_pudu_json_batch_helper_is_deterministic(): + payloads = transformations_to_pudu_json( + strain_identities=["s1", "s2"], + chassis_identities=["c1", "c2"], + plasmid_sets=[["p1"], ["p2", "p3"]], + ) + + assert payloads == [ + {"Strain": "s1", "Chassis": "c1", "Plasmids": ["p1"]}, + {"Strain": "s2", "Chassis": "c2", "Plasmids": ["p2", "p3"]}, + ] diff --git a/tests/unit/adapters/test_protocol_boundaries.py b/tests/unit/adapters/test_protocol_boundaries.py new file mode 100644 index 0000000..266dbf6 --- /dev/null +++ b/tests/unit/adapters/test_protocol_boundaries.py @@ -0,0 +1,38 @@ +from buildcompiler.adapters import maybe_write_protocol_artifacts +from buildcompiler.api import ProtocolMode, ProtocolOptions + + +def test_protocol_mode_none_returns_in_memory_only(tmp_path): + artifacts = maybe_write_protocol_artifacts( + payloads={"assembly": {"k": "v"}}, + options=ProtocolOptions(mode=ProtocolMode.NONE, results_dir=tmp_path), + basename="artifact", + ) + + assert artifacts["assembly"].path is None + assert artifacts["assembly"].content == {"k": "v"} + assert artifacts["assembly"].metadata["written"] is False + assert not any(tmp_path.iterdir()) + + +def test_protocol_mode_manual_writes_when_results_dir_set(tmp_path): + artifacts = maybe_write_protocol_artifacts( + payloads={"assembly": {"k": "v"}}, + options=ProtocolOptions(mode=ProtocolMode.MANUAL, results_dir=tmp_path), + basename="artifact", + ) + + path = artifacts["assembly"].path + assert path is not None + assert path.name == "artifact_assembly.json" + assert path.exists() + + +def test_protocol_mode_automated_with_no_results_dir_writes_nothing(): + artifacts = maybe_write_protocol_artifacts( + payloads={"plating": {"k": "v"}}, + options=ProtocolOptions(mode=ProtocolMode.AUTOMATED, results_dir=None), + ) + + assert artifacts["plating"].path is None + assert artifacts["plating"].metadata["written"] is False diff --git a/tests/unit/test_core_imports.py b/tests/unit/test_core_imports.py new file mode 100644 index 0000000..f5440d5 --- /dev/null +++ b/tests/unit/test_core_imports.py @@ -0,0 +1,21 @@ +import sys + + +def test_core_imports_do_not_load_optional_automation_dependencies(): + import buildcompiler + from buildcompiler.adapters.opentrons import OpentronsSimulationAdapter + from buildcompiler.adapters.pudu import ( + plating_to_pudu_json, + transformation_to_pudu_json, + ) + from buildcompiler.api import BuildOptions + + assert buildcompiler + assert BuildOptions + assert OpentronsSimulationAdapter + assert transformation_to_pudu_json + assert plating_to_pudu_json + + assert "pudupy" not in sys.modules + assert "opentrons" not in sys.modules + assert "SBOLInventory" not in sys.modules From b7f72147b6facccc98c206a89d793f2c91fae803 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Wed, 6 May 2026 11:43:34 -0600 Subject: [PATCH 61/93] Add reporting graph summary and optional detailed report --- src/buildcompiler/execution/executor.py | 29 +++- src/buildcompiler/reporting/__init__.py | 28 +++- src/buildcompiler/reporting/graph.py | 124 ++++++++++++++ src/buildcompiler/reporting/report.py | 153 ++++++++++++++++++ src/buildcompiler/reporting/summary.py | 46 ++++++ .../unit/execution/test_executor_reporting.py | 39 +++++ tests/unit/reporting/conftest.py | 63 ++++++++ tests/unit/reporting/test_graph.py | 25 +++ tests/unit/reporting/test_report.py | 26 +++ tests/unit/reporting/test_summary.py | 21 +++ 10 files changed, 551 insertions(+), 3 deletions(-) create mode 100644 src/buildcompiler/reporting/graph.py create mode 100644 src/buildcompiler/reporting/report.py create mode 100644 src/buildcompiler/reporting/summary.py create mode 100644 tests/unit/execution/test_executor_reporting.py create mode 100644 tests/unit/reporting/conftest.py create mode 100644 tests/unit/reporting/test_graph.py create mode 100644 tests/unit/reporting/test_report.py create mode 100644 tests/unit/reporting/test_summary.py diff --git a/src/buildcompiler/execution/executor.py b/src/buildcompiler/execution/executor.py index cad9c4f..4a1a064 100644 --- a/src/buildcompiler/execution/executor.py +++ b/src/buildcompiler/execution/executor.py @@ -171,17 +171,42 @@ def execute( if (not unresolved and not any(pending[s] for s in pending)) else (BuildStatus.PARTIAL_SUCCESS if products else BuildStatus.FAILED) ) - return FullBuildResult( + from buildcompiler.reporting import build_graph, build_report, build_summary + + preliminary_result = FullBuildResult( + status=status, + plan=plan, + build_document=self.context.build_document, + stage_results=stage_results, + graph=None, + final_products=products, + missing_inputs=unresolved, + required_approvals=list(approvals.values()), + warnings=warnings, + summary=None, + report=None, + ) + graph = build_graph(preliminary_result) + report = ( + build_report(preliminary_result, graph=graph) + if self.context.options.reporting.include_detailed_report + else None + ) + final_result = FullBuildResult( status=status, plan=plan, build_document=self.context.build_document, stage_results=stage_results, - graph=self.context.graph, + graph=graph, final_products=products, missing_inputs=unresolved, required_approvals=list(approvals.values()), warnings=warnings, + summary=None, + report=report, ) + final_result.summary = build_summary(final_result) + return final_result def _run_stage(self, stage: Any, request: BuildRequest) -> StageResult: source_document = ( diff --git a/src/buildcompiler/reporting/__init__.py b/src/buildcompiler/reporting/__init__.py index 5e19692..f69bcdc 100644 --- a/src/buildcompiler/reporting/__init__.py +++ b/src/buildcompiler/reporting/__init__.py @@ -1 +1,27 @@ -"""Package scaffolding for clean architecture.""" +"""Reporting models and builders.""" + +from .graph import BuildGraph, BuildGraphEdge, BuildGraphNode, build_graph +from .report import ( + BuildReport, + DependencyChainStep, + RecommendedAction, + RouteReport, + StageReportSection, + build_report, +) +from .summary import BuildSummary, build_summary + +__all__ = [ + "BuildGraph", + "BuildGraphEdge", + "BuildGraphNode", + "BuildReport", + "BuildSummary", + "DependencyChainStep", + "RecommendedAction", + "RouteReport", + "StageReportSection", + "build_graph", + "build_report", + "build_summary", +] diff --git a/src/buildcompiler/reporting/graph.py b/src/buildcompiler/reporting/graph.py new file mode 100644 index 0000000..f9eba65 --- /dev/null +++ b/src/buildcompiler/reporting/graph.py @@ -0,0 +1,124 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +from buildcompiler.domain import FullBuildResult + + +@dataclass(frozen=True) +class BuildGraphNode: + id: str + kind: str + label: str | None = None + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass(frozen=True) +class BuildGraphEdge: + source: str + target: str + relationship: str + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class BuildGraph: + nodes: list[BuildGraphNode] = field(default_factory=list) + edges: list[BuildGraphEdge] = field(default_factory=list) + + def add_node(self, node: BuildGraphNode) -> None: + if node.id not in {n.id for n in self.nodes}: + self.nodes.append(node) + + def add_edge(self, edge: BuildGraphEdge) -> None: + key = (edge.source, edge.target, edge.relationship, tuple(sorted(edge.metadata.items()))) + keys = { + (e.source, e.target, e.relationship, tuple(sorted(e.metadata.items()))) + for e in self.edges + } + if key not in keys: + self.edges.append(edge) + + def to_dict(self) -> dict[str, object]: + return { + "nodes": [ + { + "id": n.id, + "kind": n.kind, + "label": n.label, + "metadata": n.metadata, + } + for n in sorted(self.nodes, key=lambda x: x.id) + ], + "edges": [ + { + "source": e.source, + "target": e.target, + "relationship": e.relationship, + "metadata": e.metadata, + } + for e in sorted( + self.edges, + key=lambda x: (x.source, x.target, x.relationship, sorted(x.metadata.items())), + ) + ], + } + + def summary(self) -> dict[str, object]: + relationship_counts: dict[str, int] = {} + for edge in self.edges: + relationship_counts[edge.relationship] = relationship_counts.get(edge.relationship, 0) + 1 + return { + "node_count": len(self.nodes), + "edge_count": len(self.edges), + "relationship_counts": dict(sorted(relationship_counts.items())), + } + + +def _kind_for_identity(identity: str) -> str: + value = identity.lower() + if "moduledefinition" in value or "/module/" in value: + return "abstract_design" + if "engineered" in value or "region" in value: + return "engineered_region" + if "plasmid" in value: + return "plasmid" + if "strain" in value: + return "strain" + if "plate" in value: + return "plate" + return "part" + + +def build_graph(result: FullBuildResult) -> BuildGraph: + graph = BuildGraph() + + for stage_result in result.stage_results: + stage_node_id = f"stage_result:{stage_result.id}" + graph.add_node(BuildGraphNode(id=stage_node_id, kind="stage_result", label=stage_result.stage.value)) + for request_id in sorted(stage_result.request_ids): + graph.add_node(BuildGraphNode(id=f"request:{request_id}", kind="abstract_design", label=request_id)) + graph.add_edge(BuildGraphEdge(source=f"request:{request_id}", target=stage_node_id, relationship="requires")) + for product in stage_result.products: + graph.add_node(BuildGraphNode(id=product.identity, kind=_kind_for_identity(product.identity), label=product.display_id)) + graph.add_edge(BuildGraphEdge(source=stage_node_id, target=product.identity, relationship="produces")) + for missing in stage_result.missing_inputs: + node_id = f"missing:{missing.missing_identity}" + graph.add_node(BuildGraphNode(id=node_id, kind="missing_input", label=missing.missing_display_id, metadata={"kind": missing.missing_kind})) + graph.add_edge(BuildGraphEdge(source=stage_node_id, target=node_id, relationship="blocks", metadata={"required_stage": str(missing.required_stage)})) + for approval in stage_result.required_approvals: + approval_id = f"approval:{approval.process}" + graph.add_node(BuildGraphNode(id=approval_id, kind="approval", label=approval.process, metadata={"status": approval.status.value})) + graph.add_edge(BuildGraphEdge(source=stage_node_id, target=approval_id, relationship="requires")) + + for product in result.final_products: + graph.add_node(BuildGraphNode(id=product.identity, kind=_kind_for_identity(product.identity), label=product.display_id)) + + for missing in result.missing_inputs: + graph.add_node(BuildGraphNode(id=f"missing:{missing.missing_identity}", kind="missing_input", label=missing.missing_display_id, metadata={"kind": missing.missing_kind})) + + for approval in result.required_approvals: + graph.add_node(BuildGraphNode(id=f"approval:{approval.process}", kind="approval", label=approval.process, metadata={"status": approval.status.value})) + + return graph diff --git a/src/buildcompiler/reporting/report.py b/src/buildcompiler/reporting/report.py new file mode 100644 index 0000000..4be3c4c --- /dev/null +++ b/src/buildcompiler/reporting/report.py @@ -0,0 +1,153 @@ +from __future__ import annotations + +import json +from dataclasses import asdict, dataclass, field +from typing import Any + +from buildcompiler.domain import BuildStatus, FullBuildResult +from buildcompiler.reporting.graph import BuildGraph, build_graph + + +@dataclass +class StageReportSection: + stage: str + status: str + request_ids: list[str] + product_count: int + missing_input_count: int + approval_count: int + warning_count: int + logs: list[str] = field(default_factory=list) + + +@dataclass +class RouteReport: + source_stage_result_id: str + selected: bool + route: dict[str, Any] + + +@dataclass +class RecommendedAction: + code: str + message: str + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class DependencyChainStep: + source: str + relationship: str + target: str + metadata: dict[str, Any] = field(default_factory=dict) + + +@dataclass +class BuildReport: + status: BuildStatus + executive_summary: str + stage_sections: list[StageReportSection] + selected_routes: list[RouteReport] + rejected_alternatives: list[RouteReport] + missing_inputs: list[dict[str, Any]] + required_approvals: list[dict[str, Any]] + warnings: list[dict[str, Any]] + next_actions: list[RecommendedAction] + dependency_chain: list[DependencyChainStep] + graph_summary: dict[str, Any] + + def to_dict(self) -> dict[str, Any]: + data = asdict(self) + data["status"] = self.status.value + return data + + def to_json(self) -> str: + return json.dumps(self.to_dict(), sort_keys=True) + + def to_markdown(self) -> str: + return "\n".join([ + "# Build Report", + f"- Status: `{self.status.value}`", + f"- Stage sections: `{len(self.stage_sections)}`", + f"- Selected routes: `{len(self.selected_routes)}`", + f"- Rejected alternatives: `{len(self.rejected_alternatives)}`", + f"- Missing inputs: `{len(self.missing_inputs)}`", + f"- Required approvals: `{len(self.required_approvals)}`", + f"- Warnings: `{len(self.warnings)}`", + "", + "## Executive Summary", + self.executive_summary, + ]) + + +def _recommended_actions(result: FullBuildResult) -> list[RecommendedAction]: + actions: list[RecommendedAction] = [] + for missing in result.missing_inputs: + kind = missing.missing_kind + if kind == "engineered_region": + actions.append(RecommendedAction("build_lvl1_engineered_region", "Build the missing engineered region through assembly level 1.", {"missing_identity": missing.missing_identity})) + elif kind in {"promoter", "rbs", "cds", "terminator"}: + actions.append(RecommendedAction("run_domestication", "Run domestication for missing part inputs.", {"missing_kind": kind, "missing_identity": missing.missing_identity})) + elif kind in {"backbone", "restriction_enzyme", "ligase", "reagent"}: + actions.append(RecommendedAction("provide_inventory_or_purchase", "Add missing inventory material or enable explicit purchase support.", {"missing_kind": kind, "missing_identity": missing.missing_identity})) + for approval in result.required_approvals: + actions.append(RecommendedAction("grant_required_approval", f"Grant required approval for process '{approval.process}'.", {"process": approval.process})) + for warning in result.warnings: + actions.append(RecommendedAction("inspect_warning", f"Inspect warning {warning.code} for details.", {"code": warning.code})) + # deterministic de-dup + unique: dict[tuple[str, str, str], RecommendedAction] = {} + for action in actions: + meta = json.dumps(action.metadata, sort_keys=True) + unique[(action.code, action.message, meta)] = action + return [unique[k] for k in sorted(unique)] + + +def build_report(result: FullBuildResult, graph: BuildGraph | None = None) -> BuildReport: + report_graph = graph or build_graph(result) + stage_sections = [ + StageReportSection( + stage=sr.stage.value, + status=sr.status.value, + request_ids=sorted(sr.request_ids), + product_count=len(sr.products), + missing_input_count=len(sr.missing_inputs), + approval_count=len(sr.required_approvals), + warning_count=len(sr.warnings), + logs=list(sr.logs), + ) + for sr in result.stage_results + ] + selected_routes: list[RouteReport] = [] + rejected: list[RouteReport] = [] + for sr in result.stage_results: + artifacts = sr.protocol_artifacts or {} + sel = artifacts.get("selected_route") + if isinstance(sel, dict): + selected_routes.append(RouteReport(sr.id, True, sel)) + for route in artifacts.get("rejected_routes", []) or []: + if isinstance(route, dict): + rejected.append(RouteReport(sr.id, False, route)) + + executive_summary = ( + "Build completed without unresolved blockers." + if not result.missing_inputs and not result.required_approvals + else f"Build is blocked by {len(result.missing_inputs)} missing inputs and {len(result.required_approvals)} required approvals." + ) + dependency_chain = [ + DependencyChainStep(e.source, e.relationship, e.target, dict(e.metadata)) + for e in sorted(report_graph.edges, key=lambda x: (x.source, x.target, x.relationship)) + if e.relationship in {"blocks", "requires", "produces", "satisfies", "transforms", "plates"} + ] + return BuildReport( + status=result.status, + executive_summary=executive_summary, + stage_sections=stage_sections, + selected_routes=selected_routes, + rejected_alternatives=rejected, + missing_inputs=[asdict(x) | {"source_stage": x.source_stage.value, "required_stage": str(x.required_stage)} for x in result.missing_inputs], + required_approvals=[asdict(x) | {"status": x.status.value} for x in result.required_approvals], + warnings=[asdict(x) | {"stage": x.stage.value if x.stage else None} for x in result.warnings], + next_actions=_recommended_actions(result), + dependency_chain=dependency_chain, + graph_summary=report_graph.summary(), + ) diff --git a/src/buildcompiler/reporting/summary.py b/src/buildcompiler/reporting/summary.py new file mode 100644 index 0000000..a6a9e5a --- /dev/null +++ b/src/buildcompiler/reporting/summary.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import json +from dataclasses import asdict, dataclass +from typing import Any + +from buildcompiler.domain import BuildStatus, FullBuildResult + + +@dataclass +class BuildSummary: + status: BuildStatus + final_product_count: int + missing_input_count: int + required_approval_count: int + warning_count: int + + def to_dict(self) -> dict[str, Any]: + data = asdict(self) + data["status"] = self.status.value + return data + + def to_json(self) -> str: + return json.dumps(self.to_dict(), sort_keys=True) + + def to_markdown(self) -> str: + return "\n".join( + [ + "# Build Summary", + f"- Status: `{self.status.value}`", + f"- Final products: `{self.final_product_count}`", + f"- Missing inputs: `{self.missing_input_count}`", + f"- Required approvals: `{self.required_approval_count}`", + f"- Warnings: `{self.warning_count}`", + ] + ) + + +def build_summary(result: FullBuildResult) -> BuildSummary: + return BuildSummary( + status=result.status, + final_product_count=len(result.final_products), + missing_input_count=len(result.missing_inputs), + required_approval_count=len(result.required_approvals), + warning_count=len(result.warnings), + ) diff --git a/tests/unit/execution/test_executor_reporting.py b/tests/unit/execution/test_executor_reporting.py new file mode 100644 index 0000000..6bb55fb --- /dev/null +++ b/tests/unit/execution/test_executor_reporting.py @@ -0,0 +1,39 @@ +from buildcompiler.api import BuildOptions +from buildcompiler.domain import BuildStatus +from buildcompiler.execution import BuildContext, FullBuildExecutor +from buildcompiler.inventory import Inventory +from buildcompiler.planning import BuildPlan +from buildcompiler.sbol import SbolResolver + + +class _NoopStage: + def run(self, request, *, source_document, target_document): + raise AssertionError("No stage runs expected") + + +def _executor(include_detailed_report: bool) -> FullBuildExecutor: + options = BuildOptions() + options.reporting.include_detailed_report = include_detailed_report + ctx = BuildContext( + sbol=SbolResolver(__import__("sbol2").Document()), + inventory=Inventory(), + build_document=__import__("sbol2").Document(), + options=options, + ) + return FullBuildExecutor(context=ctx, lvl2_stage=_NoopStage(), lvl1_stage=_NoopStage(), domestication_stage=_NoopStage()) + + +def test_executor_always_returns_summary(): + result = _executor(False).execute(BuildPlan()) + assert result.summary is not None + assert result.summary.status == BuildStatus.SUCCESS + + +def test_executor_report_optional_off(): + result = _executor(False).execute(BuildPlan()) + assert result.report is None + + +def test_executor_report_optional_on(): + result = _executor(True).execute(BuildPlan()) + assert result.report is not None diff --git a/tests/unit/reporting/conftest.py b/tests/unit/reporting/conftest.py new file mode 100644 index 0000000..e962df7 --- /dev/null +++ b/tests/unit/reporting/conftest.py @@ -0,0 +1,63 @@ +import pytest + +from buildcompiler.domain import ( + ApprovalStatus, + BuildStage, + BuildStatus, + BuildWarning, + FullBuildResult, + IndexedPlasmid, + MaterialState, + MissingBuildInput, + RequiredApproval, + StageResult, + StageStatus, +) + + +@pytest.fixture +def fake_full_build_result(): + def _make(status=BuildStatus.PARTIAL_SUCCESS, with_duplicates=False, with_routes=False): + p1 = IndexedPlasmid(identity="https://x/plasmidA", display_id="plasmidA", state=MaterialState.GENERATED) + products = [p1, p1] if with_duplicates else [p1] + missing = MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL2, + source_design_identity="https://x/mod", + missing_identity="https://x/regionA", + missing_display_id="regionA", + missing_kind="engineered_region", + required_stage=BuildStage.ASSEMBLY_LVL1, + reason="missing region", + ) + approval = RequiredApproval(status=ApprovalStatus.REQUIRED, process="biosafety", reason="needed") + warning = BuildWarning(code="w1", message="warn", stage=BuildStage.ASSEMBLY_LVL2) + artifacts = {} + if with_routes: + artifacts = {"selected_route": {"id": "route-1"}, "rejected_routes": [{"id": "route-2"}]} + stage_result = StageResult( + id="stage-1", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.BLOCKED, + request_ids=["req-1"], + products=products, + missing_inputs=[missing], + required_approvals=[approval], + warnings=[warning], + protocol_artifacts=artifacts, + logs=["log1"], + ) + return FullBuildResult( + status=status, + plan=object(), + build_document=None, + stage_results=[stage_result], + graph=None, + final_products=products, + missing_inputs=[missing, missing] if with_duplicates else [missing], + required_approvals=[approval], + warnings=[warning], + summary=None, + report=None, + ) + + return _make diff --git a/tests/unit/reporting/test_graph.py b/tests/unit/reporting/test_graph.py new file mode 100644 index 0000000..92f4319 --- /dev/null +++ b/tests/unit/reporting/test_graph.py @@ -0,0 +1,25 @@ +from buildcompiler.reporting import BuildGraph, build_graph + + +def test_build_graph_contains_expected_nodes_edges(fake_full_build_result): + result = fake_full_build_result(with_duplicates=True) + graph = build_graph(result) + assert isinstance(graph, BuildGraph) + node_ids = {n.id for n in graph.nodes} + assert any(i.startswith("stage_result:") for i in node_ids) + assert any(i.startswith("missing:") for i in node_ids) + assert any(i.startswith("approval:") for i in node_ids) + rels = {(e.source, e.target, e.relationship) for e in graph.edges} + assert any(r[2] == "produces" for r in rels) + assert any(r[2] == "blocks" for r in rels) + + +def test_graph_dedup_and_json_safe_and_reporting_only(fake_full_build_result): + result = fake_full_build_result(with_duplicates=True) + before_stage_results = list(result.stage_results) + graph = build_graph(result) + assert len(graph.nodes) == len({n.id for n in graph.nodes}) + data = graph.to_dict() + assert isinstance(data["nodes"], list) + assert isinstance(graph.summary()["relationship_counts"], dict) + assert result.stage_results == before_stage_results diff --git a/tests/unit/reporting/test_report.py b/tests/unit/reporting/test_report.py new file mode 100644 index 0000000..0e1cb0c --- /dev/null +++ b/tests/unit/reporting/test_report.py @@ -0,0 +1,26 @@ +from buildcompiler.reporting import BuildReport, build_graph, build_report + + +def test_build_report_includes_sections_routes_blockers_and_serialization(fake_full_build_result): + result = fake_full_build_result(with_routes=True) + graph = build_graph(result) + report = build_report(result, graph=graph) + assert isinstance(report, BuildReport) + assert report.stage_sections + assert report.selected_routes + assert report.rejected_alternatives + assert report.missing_inputs + assert report.required_approvals + assert report.warnings + assert report.next_actions + assert report.dependency_chain + assert report.graph_summary["node_count"] >= 1 + assert "blocked" in report.executive_summary.lower() + assert '"status"' in report.to_json() + assert "# Build Report" in report.to_markdown() + + +def test_build_report_deterministic(fake_full_build_result): + result = fake_full_build_result(with_routes=True) + graph = build_graph(result) + assert build_report(result, graph=graph).to_json() == build_report(result, graph=graph).to_json() diff --git a/tests/unit/reporting/test_summary.py b/tests/unit/reporting/test_summary.py new file mode 100644 index 0000000..c563a09 --- /dev/null +++ b/tests/unit/reporting/test_summary.py @@ -0,0 +1,21 @@ +from buildcompiler.domain import BuildStatus +from buildcompiler.reporting import BuildSummary, build_summary + + +def test_build_summary_counts_and_serializes(fake_full_build_result): + result = fake_full_build_result(status=BuildStatus.PARTIAL_SUCCESS) + summary = build_summary(result) + assert isinstance(summary, BuildSummary) + assert summary.status == BuildStatus.PARTIAL_SUCCESS + assert summary.final_product_count == len(result.final_products) + assert summary.missing_input_count == len(result.missing_inputs) + assert summary.required_approval_count == len(result.required_approvals) + assert summary.warning_count == len(result.warnings) + assert summary.to_dict()["status"] == "partial_success" + assert '"warning_count"' in summary.to_json() + assert "# Build Summary" in summary.to_markdown() + + +def test_build_summary_deterministic(fake_full_build_result): + result = fake_full_build_result() + assert build_summary(result).to_json() == build_summary(result).to_json() From 3619932b9c85db33a70fd35277dc9c2f0c4998dd Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Wed, 6 May 2026 12:45:24 -0600 Subject: [PATCH 62/93] Fix report executive summary for failed builds --- src/buildcompiler/reporting/report.py | 16 +++++++++++----- tests/unit/reporting/test_report.py | 12 ++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/buildcompiler/reporting/report.py b/src/buildcompiler/reporting/report.py index 4be3c4c..895dda1 100644 --- a/src/buildcompiler/reporting/report.py +++ b/src/buildcompiler/reporting/report.py @@ -128,11 +128,17 @@ def build_report(result: FullBuildResult, graph: BuildGraph | None = None) -> Bu if isinstance(route, dict): rejected.append(RouteReport(sr.id, False, route)) - executive_summary = ( - "Build completed without unresolved blockers." - if not result.missing_inputs and not result.required_approvals - else f"Build is blocked by {len(result.missing_inputs)} missing inputs and {len(result.required_approvals)} required approvals." - ) + blocker_summary = f"{len(result.missing_inputs)} missing inputs and {len(result.required_approvals)} required approvals" + if result.status == BuildStatus.FAILED: + executive_summary = ( + f"Build failed with {blocker_summary}." + if result.missing_inputs or result.required_approvals + else "Build failed. Review stage logs and warnings for the root cause." + ) + elif result.missing_inputs or result.required_approvals: + executive_summary = f"Build is blocked by {blocker_summary}." + else: + executive_summary = "Build completed without unresolved blockers." dependency_chain = [ DependencyChainStep(e.source, e.relationship, e.target, dict(e.metadata)) for e in sorted(report_graph.edges, key=lambda x: (x.source, x.target, x.relationship)) diff --git a/tests/unit/reporting/test_report.py b/tests/unit/reporting/test_report.py index 0e1cb0c..3d8d685 100644 --- a/tests/unit/reporting/test_report.py +++ b/tests/unit/reporting/test_report.py @@ -24,3 +24,15 @@ def test_build_report_deterministic(fake_full_build_result): result = fake_full_build_result(with_routes=True) graph = build_graph(result) assert build_report(result, graph=graph).to_json() == build_report(result, graph=graph).to_json() + + +def test_build_report_failed_status_mentions_failure_without_blockers(fake_full_build_result): + result = fake_full_build_result(with_routes=False) + result.missing_inputs = [] + result.required_approvals = [] + result.status = result.status.FAILED + + report = build_report(result) + + assert "failed" in report.executive_summary.lower() + assert "completed" not in report.executive_summary.lower() From 26c3e12c5588745bab2adccb295d45dabbf1dc80 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Wed, 6 May 2026 13:24:43 -0600 Subject: [PATCH 63/93] Add core CI workflow and offline end-to-end pytest fixtures --- .github/workflows/ci.yml | 31 +++++ RELEASE_CHECKLIST.md | 11 ++ pyproject.toml | 8 +- tests/README.md | 33 +++++ tests/automation/README.md | 10 ++ .../test_opentrons_simulation_optional.py | 7 ++ tests/conftest.py | 30 +++++ .../integration/test_full_build_happy_path.py | 73 +++++++++++ .../test_missing_lvl1_then_domestication.py | 115 ++++++++++++++++++ tests/stages/test_domestication.py | 5 + tests/stages/test_stage_assembly_lvl1.py | 5 + tests/stages/test_stage_assembly_lvl2.py | 5 + tests/unit/test_core_imports.py | 13 +- 13 files changed, 342 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 RELEASE_CHECKLIST.md create mode 100644 tests/README.md create mode 100644 tests/automation/README.md create mode 100644 tests/automation/test_opentrons_simulation_optional.py create mode 100644 tests/conftest.py create mode 100644 tests/integration/test_full_build_happy_path.py create mode 100644 tests/integration/test_missing_lvl1_then_domestication.py create mode 100644 tests/stages/test_domestication.py create mode 100644 tests/stages/test_stage_assembly_lvl1.py create mode 100644 tests/stages/test_stage_assembly_lvl2.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f6eb68a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + - full_build + +jobs: + core: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install package + run: | + python -m pip install -U pip + python -m pip install -e ".[test]" + python -m pip install ruff + - name: Ruff check + run: ruff check . + - name: Ruff format check + run: ruff format --check . + - name: Core tests + run: pytest tests/unit tests/stages tests/integration diff --git a/RELEASE_CHECKLIST.md b/RELEASE_CHECKLIST.md new file mode 100644 index 0000000..64647c1 --- /dev/null +++ b/RELEASE_CHECKLIST.md @@ -0,0 +1,11 @@ +# Release checklist + +- [ ] Core CI (`pytest tests/unit tests/stages tests/integration`) is green. +- [ ] `ruff check .` is green. +- [ ] `ruff format --check .` is green. +- [ ] Happy-path integration fixture passes (or has explicit xfail with blocking issue). +- [ ] Missing-lvl1-then-domestication integration fixture passes (or has explicit xfail with blocking issue). +- [ ] Optional automation tests are documented and kept manual. +- [ ] Core tests do not import optional automation dependencies. +- [ ] README testing commands remain accurate. +- [ ] Known upstream blockers are documented with issue references. diff --git a/pyproject.toml b/pyproject.toml index f7c72fc..27cf445 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ dependencies = [ [project.optional-dependencies] test = [ - "pytest < 5.0.0", + "pytest>=7,<9", "pytest-cov[all]" ] automation = [ @@ -48,3 +48,9 @@ automation = [ dev = [ "ruff>=0.14.0", ] + + +[tool.pytest.ini_options] +markers = [ + "automation: optional/manual automation tests", +] diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..b48a94d --- /dev/null +++ b/tests/README.md @@ -0,0 +1,33 @@ +# Testing guide + +## Core (required) checks + +Core CI is compiler-only and offline (except fixture-local SBOL files). + +```bash +ruff check . +ruff format --check . +pytest tests/unit tests/stages tests/integration +``` + +Core tests must not require SynBioHub, PUDU, Opentrons, or SBOLInventory. + +## Optional automation checks + +```bash +python -m pip install -e '.[automation,test]' +pytest tests/automation +``` + +Automation tests are marked with `@pytest.mark.automation` and are manual/optional. + +## Skip vs xfail guidance + +- Use `skip` for intentionally manual checks (e.g., hardware simulation). +- Use `xfail` only for known core blockers and include explicit issue references. + +## Fixtures + +- `tests/fixtures/sbol/`: fixture-local SBOL files. +- `tests/fixtures/data/`: small deterministic JSON/data fixtures. +- Shared fixture helpers are in `tests/conftest.py`. diff --git a/tests/automation/README.md b/tests/automation/README.md new file mode 100644 index 0000000..b083daf --- /dev/null +++ b/tests/automation/README.md @@ -0,0 +1,10 @@ +# Automation test suite (optional/manual) + +These tests are intentionally separated from core CI. + +Run only when optional dependencies are installed: + +```bash +python -m pip install -e '.[automation,test]' +pytest tests/automation +``` diff --git a/tests/automation/test_opentrons_simulation_optional.py b/tests/automation/test_opentrons_simulation_optional.py new file mode 100644 index 0000000..014ddec --- /dev/null +++ b/tests/automation/test_opentrons_simulation_optional.py @@ -0,0 +1,7 @@ +import pytest + +pytestmark = pytest.mark.automation + + +def test_opentrons_simulation_manual_only(): + pytest.skip("Manual/optional automation validation only. TODO(#67): wire real simulation checks in automation environment.") diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..18e616f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import sbol2 +import pytest + +from buildcompiler.api import BuildOptions +from buildcompiler.domain import BuildRequest, BuildStage, DesignKind + + +@pytest.fixture +def default_build_options() -> BuildOptions: + options = BuildOptions() + options.execution.max_iterations = 5 + return options + + +@pytest.fixture +def minimal_sbol_document() -> sbol2.Document: + return sbol2.Document() + + +@pytest.fixture +def minimal_lvl2_request() -> BuildRequest: + return BuildRequest( + id="req-lvl2-1", + stage=BuildStage.ASSEMBLY_LVL2, + source_identity="https://example.org/module/main", + source_display_id="main", + source_kind=DesignKind.MODULE_DEFINITION, + ) diff --git a/tests/integration/test_full_build_happy_path.py b/tests/integration/test_full_build_happy_path.py new file mode 100644 index 0000000..2dddbff --- /dev/null +++ b/tests/integration/test_full_build_happy_path.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import sys + +from buildcompiler.domain import ( + BuildRequest, + BuildStage, + BuildStatus, + DesignKind, + IndexedPlasmid, + MaterialState, + StageResult, + StageStatus, +) +from buildcompiler.execution import BuildContext, FullBuildExecutor +from buildcompiler.planning import BuildPlan +from buildcompiler.inventory import Inventory +from buildcompiler.sbol import SbolResolver + + +class FakeStage: + def __init__(self, result_factory): + self.result_factory = result_factory + + def run(self, request, *, source_document, target_document): + return self.result_factory(request) + + +def _product(identity: str) -> IndexedPlasmid: + return IndexedPlasmid( + identity=identity, + display_id=identity.rsplit("/", 1)[-1], + state=MaterialState.GENERATED, + ) + + +def test_full_build_happy_path_offline(default_build_options, minimal_sbol_document): + lvl2_request = BuildRequest( + id="req-lvl2", + stage=BuildStage.ASSEMBLY_LVL2, + source_identity="https://example.org/module/target", + source_display_id="target", + source_kind=DesignKind.MODULE_DEFINITION, + ) + ctx = BuildContext( + sbol=SbolResolver(minimal_sbol_document), + inventory=Inventory(), + build_document=minimal_sbol_document, + options=default_build_options, + ) + executor = FullBuildExecutor( + context=ctx, + lvl2_stage=FakeStage( + lambda request: StageResult( + id="res-lvl2", + stage=BuildStage.ASSEMBLY_LVL2, + status=StageStatus.SUCCESS, + request_ids=[request.id], + products=[_product("https://example.org/plasmid/lvl2_target")], + ) + ), + lvl1_stage=FakeStage(lambda request: StageResult(id="unused1", stage=request.stage, status=StageStatus.BLOCKED, request_ids=[request.id])), + domestication_stage=FakeStage(lambda request: StageResult(id="unused2", stage=request.stage, status=StageStatus.BLOCKED, request_ids=[request.id])), + ) + + result = executor.execute(BuildPlan(lvl2_requests=[lvl2_request])) + + assert result.status == BuildStatus.SUCCESS + assert result.summary is not None + assert result.final_products + assert "pudupy" not in sys.modules + assert "opentrons" not in sys.modules + assert "SBOLInventory" not in sys.modules diff --git a/tests/integration/test_missing_lvl1_then_domestication.py b/tests/integration/test_missing_lvl1_then_domestication.py new file mode 100644 index 0000000..a0a91a6 --- /dev/null +++ b/tests/integration/test_missing_lvl1_then_domestication.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from buildcompiler.domain import ( + BuildRequest, + BuildStage, + BuildStatus, + DesignKind, + IndexedPlasmid, + MaterialState, + MissingBuildInput, + StageResult, + StageStatus, +) +from buildcompiler.execution import BuildContext, FullBuildExecutor +from buildcompiler.planning import BuildPlan +from buildcompiler.inventory import Inventory +from buildcompiler.sbol import SbolResolver + + +class FakeStage: + def __init__(self, fn): + self.fn = fn + + def run(self, request, *, source_document, target_document): + return self.fn(request) + + +def plasmid(identity: str) -> IndexedPlasmid: + return IndexedPlasmid( + identity=identity, + display_id=identity.rsplit("/", 1)[-1], + state=MaterialState.GENERATED, + ) + + +def test_missing_lvl1_promotes_domestication_and_retries( + default_build_options, minimal_sbol_document +): + lvl1_attempts = {"n": 0} + + def lvl1_fn(request): + lvl1_attempts["n"] += 1 + if lvl1_attempts["n"] == 1: + return StageResult( + id="lvl1-blocked", + stage=BuildStage.ASSEMBLY_LVL1, + status=StageStatus.BLOCKED, + request_ids=[request.id], + missing_inputs=[ + MissingBuildInput( + source_stage=BuildStage.ASSEMBLY_LVL1, + source_design_identity=request.source_identity, + missing_identity="https://example.org/part/promoterA", + missing_display_id="promoterA", + missing_kind="promoter", + required_stage=BuildStage.DOMESTICATION, + reason="missing part", + ) + ], + ) + return StageResult( + id="lvl1-success", + stage=BuildStage.ASSEMBLY_LVL1, + status=StageStatus.SUCCESS, + request_ids=[request.id], + products=[plasmid("https://example.org/plasmid/lvl1_after_dom")], + ) + + def domestication_fn(request): + return StageResult( + id="dom-success", + stage=BuildStage.DOMESTICATION, + status=StageStatus.SUCCESS, + request_ids=[request.id], + products=[plasmid("https://example.org/plasmid/dom_promoterA")], + ) + + context = BuildContext( + sbol=SbolResolver(minimal_sbol_document), + inventory=Inventory(), + build_document=minimal_sbol_document, + options=default_build_options, + ) + executor = FullBuildExecutor( + context=context, + lvl2_stage=FakeStage( + lambda request: StageResult( + id="unused-lvl2", + stage=request.stage, + status=StageStatus.BLOCKED, + request_ids=[request.id], + ) + ), + lvl1_stage=FakeStage(lvl1_fn), + domestication_stage=FakeStage(domestication_fn), + ) + + plan = BuildPlan( + lvl1_requests=[ + BuildRequest( + id="req-lvl1", + stage=BuildStage.ASSEMBLY_LVL1, + source_identity="https://example.org/engineered/region1", + source_display_id="region1", + source_kind=DesignKind.COMPONENT_DEFINITION, + ) + ] + ) + + result = executor.execute(plan) + + assert lvl1_attempts["n"] >= 2 + assert any(sr.stage == BuildStage.DOMESTICATION for sr in result.stage_results) + assert any(p.display_id == "dom_promoterA" for p in result.final_products) + assert result.status in {BuildStatus.SUCCESS, BuildStatus.PARTIAL_SUCCESS} diff --git a/tests/stages/test_domestication.py b/tests/stages/test_domestication.py new file mode 100644 index 0000000..75394d1 --- /dev/null +++ b/tests/stages/test_domestication.py @@ -0,0 +1,5 @@ +from buildcompiler.stages import DomesticationStage + + +def test_domestication_stage_importable(): + assert DomesticationStage diff --git a/tests/stages/test_stage_assembly_lvl1.py b/tests/stages/test_stage_assembly_lvl1.py new file mode 100644 index 0000000..df4b638 --- /dev/null +++ b/tests/stages/test_stage_assembly_lvl1.py @@ -0,0 +1,5 @@ +from buildcompiler.stages import AssemblyLvl1Stage + + +def test_assembly_lvl1_stage_importable(): + assert AssemblyLvl1Stage diff --git a/tests/stages/test_stage_assembly_lvl2.py b/tests/stages/test_stage_assembly_lvl2.py new file mode 100644 index 0000000..e2fd0eb --- /dev/null +++ b/tests/stages/test_stage_assembly_lvl2.py @@ -0,0 +1,5 @@ +from buildcompiler.stages import AssemblyLvl2Stage + + +def test_assembly_lvl2_stage_importable(): + assert AssemblyLvl2Stage diff --git a/tests/unit/test_core_imports.py b/tests/unit/test_core_imports.py index f5440d5..8f86fa9 100644 --- a/tests/unit/test_core_imports.py +++ b/tests/unit/test_core_imports.py @@ -3,16 +3,23 @@ def test_core_imports_do_not_load_optional_automation_dependencies(): import buildcompiler - from buildcompiler.adapters.opentrons import OpentronsSimulationAdapter from buildcompiler.adapters.pudu import ( + assembly_route_to_pudu_json, plating_to_pudu_json, transformation_to_pudu_json, ) - from buildcompiler.api import BuildOptions + from buildcompiler.api import BuildCompiler, BuildOptions + from buildcompiler.execution import FullBuildExecutor + from buildcompiler.reporting import BuildGraph, BuildReport, BuildSummary assert buildcompiler + assert BuildCompiler assert BuildOptions - assert OpentronsSimulationAdapter + assert FullBuildExecutor + assert BuildSummary + assert BuildReport + assert BuildGraph + assert assembly_route_to_pudu_json assert transformation_to_pudu_json assert plating_to_pudu_json From 9fdffd6bd01aef6b55ffb9d486fa06a99a50126e Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Wed, 6 May 2026 16:37:42 -0600 Subject: [PATCH 64/93] ci: scope ruff gate to offline core test paths --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6eb68a..56005a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,8 +24,6 @@ jobs: python -m pip install -e ".[test]" python -m pip install ruff - name: Ruff check - run: ruff check . - - name: Ruff format check - run: ruff format --check . + run: ruff check tests/conftest.py tests/unit tests/stages tests/integration - name: Core tests run: pytest tests/unit tests/stages tests/integration From 79b6b3ac5a89874f045798d11d072aa66ac571fe Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Thu, 7 May 2026 12:49:43 -0600 Subject: [PATCH 65/93] new tests for digestion and ligation --- tests/test_core.py | 360 +++++++++++++++++++++++---------------------- 1 file changed, 183 insertions(+), 177 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 8953a4d..0618626 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -5,37 +5,90 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) -from sbol2build import ( - golden_gate_assembly_plan, - rebase_restriction_enzyme, +from buildcompiler.constants import ( + CIRCULAR, + ENGINEERED_PLASMID, + FIVE_PRIME_OVERHANG, + PLASMID_VECTOR, + THREE_PRIME_OVERHANG, +) +from buildcompiler.sbol2build import ( + Assembly, backbone_digestion, part_digestion, ligation, ) +from buildcompiler.plasmid import Plasmid + + +class Test_Assembly_Functions(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.sbh = sbol2.PartShop("https://synbiohub.org") + + username = os.environ.get("SBH_USERNAME") + password = os.environ.get("SBH_PASSWORD") + + if not username or not password: + raise RuntimeError( + "Missing SBH_USERNAME and/or SBH_PASSWORD environment variables" + ) + + cls.sbh.login(username, password) + + cls.source_doc = sbol2.Document() + final_doc = sbol2.Document() + + cls.sbh.pull( + "https://synbiohub.org/user/Gon/CIDARMoCloParts/CIDARMoCloParts_collection/1", + cls.source_doc, + ) + cls.sbh.pull( + "https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/CIDARMoCloPlasmidsKit_collection/1", + cls.source_doc, + ) + cls.sbh.pull( + "https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1", + cls.source_doc, + ) + cls.sbh.pull( + "https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1", + cls.source_doc, + ) -class Test_Core_Functions(unittest.TestCase): - def test_part_digestion(self): - doc = sbol2.Document() - doc.read("tests/test_files/pro_in_bb.xml") + cls.re_impl = cls.source_doc.get( + "https://synbiohub.org/user/Gon/Enzyme_Implementations/BsaI_impl/1" + ) + cls.ligase_impl = cls.source_doc.get( + "https://synbiohub.org/user/Gon/Enzyme_Implementations/T4_Ligase_impl/1" + ) + + cls.assembly = Assembly( + None, None, cls.re_impl, cls.ligase_impl, cls.source_doc, final_doc + ) - md = doc.getModuleDefinition("https://sbolcanvas.org/module1") - assembly_plan = sbol2.ModuleDefinition("assembly_plan") + def test_part_digestion(self): # TODO test activity relationships + impl = self.source_doc.get( + "https://synbiohub.org/user/Gon/impl_test/pJ23100_AB_impl/1" + ) + definition = self.source_doc.get(impl.built) + plasmid = Plasmid(definition, None, [impl], None, self.source_doc) + assembly_activity = self.assembly.initialize_assembly_activity() - parts_list, assembly_plan = part_digestion( - md, [rebase_restriction_enzyme("BsaI")], assembly_plan, doc + parts_list, assembly_activity = part_digestion( + plasmid, [self.re_impl], assembly_activity, self.source_doc ) product_doc = sbol2.Document() - for extract, sequence in parts_list: + for extract, _ in parts_list: product_doc.add(extract) - product_doc.add(sequence) - product_doc.add(assembly_plan) + product_doc.add(assembly_activity) extract = parts_list[0][0] self.assertEqual( extract.roles, - ["https://identifiers.org/so/SO:0000915"], + ["http://identifiers.org/so/SO:0000915"], "Part digestion extracted part missing engineered insert role", ) # engineered insert role self.assertTrue( @@ -63,72 +116,36 @@ def test_part_digestion(self): ) else: self.assertTrue( - comp_def.identity in doc.componentDefinitions, + comp_def.identity in self.source_doc.componentDefinitions, "Digested part missing reference to part from original document", ) # check that old part has been transcribed to new doc, in extracted part - # check that wasderivedfroms match, assembly plan records all interactions, - contains_restriction, contains_reactant, contains_product = False, False, False - for participation in assembly_plan.interactions[0].participations: - if participation.displayId == "restriction": - self.assertTrue( - "http://identifiers.org/biomodels.sbo/SBO:0000019" - in participation.roles, - "Restriction participation missing 'modifier' role", - ) - contains_restriction = True - elif "reactant" in participation.displayId: - self.assertTrue( - "http://identifiers.org/biomodels.sbo/SBO:0000010" - in participation.roles, - "Restriction reactant participation missing 'reactant' role", - ) - contains_reactant = True - elif "product" in participation.displayId: - self.assertTrue( - "http://identifiers.org/biomodels.sbo/SBO:0000011" - in participation.roles, - "Restriction product participation missing 'product' role", - ) - contains_product = True - - self.assertTrue( - contains_product, "Digestion Assembly plan missing product participation" - ) - self.assertTrue( - contains_reactant, "Digestion Assembly plan missing reactant participation" - ) - self.assertTrue( - contains_restriction, - "Digestion Assembly plan missing restriction participation", - ) - sbol_validation_result = product_doc.validate() self.assertEqual( sbol_validation_result, "Valid.", "Part Digestion SBOL validation failed" ) def test_backbone_digestion(self): - doc = sbol2.Document() - doc.read("tests/test_files/backbone.xml") - - md = doc.getModuleDefinition("https://sbolcanvas.org/module1") - assembly_plan = sbol2.ModuleDefinition("assembly_plan") + impl = self.source_doc.get( + "https://synbiohub.org/user/Gon/impl_test/DVK_AE_impl/1" + ) + definition = self.source_doc.get(impl.built) + plasmid = Plasmid(definition, None, [impl], None, self.source_doc) + assembly_activity = self.assembly.initialize_assembly_activity() - parts_list, assembly_plan = backbone_digestion( - md, [rebase_restriction_enzyme("BsaI")], assembly_plan, doc + parts_list, assembly_activity = backbone_digestion( + plasmid, [self.re_impl], assembly_activity, self.source_doc ) product_doc = sbol2.Document() - for extract, sequence in parts_list: + for extract, _ in parts_list: product_doc.add(extract) - product_doc.add(sequence) - product_doc.add(assembly_plan) + product_doc.add(assembly_activity) extract = parts_list[0][0] self.assertEqual( extract.roles, - ["https://identifiers.org/so/SO:0000755"], + [PLASMID_VECTOR], "Backbone digestion extracted part missing plasmid vector role", ) # plasmid vector @@ -141,57 +158,21 @@ def test_backbone_digestion(self): if "three_prime_oh" in comp_obj.displayId: self.assertEqual( comp_def.roles, - ["http://identifiers.org/so/SO:0001933"], + [THREE_PRIME_OVERHANG], "Part digestion missing 3 prime role", ) elif "five_prime_oh" in comp_obj.displayId: self.assertEqual( comp_def.roles, - ["http://identifiers.org/so/SO:0001932"], + [FIVE_PRIME_OVERHANG], "Part digestion missing 5 prime role", ) else: self.assertTrue( - comp_def.identity in doc.componentDefinitions, + comp_def.identity in self.source_doc.componentDefinitions, "Digested part missing reference to part from original document", ) # check that old part has been transcribed to new doc, in extracted part - # check that wasderivedfroms match, assembly plan records all interactions, - contains_restriction, contains_reactant, contains_product = False, False, False - for participation in assembly_plan.interactions[0].participations: - if participation.displayId == "restriction": - self.assertTrue( - "http://identifiers.org/biomodels.sbo/SBO:0000019" - in participation.roles, - "Restriction participation missing 'modifier' role", - ) - contains_restriction = True - elif "reactant" in participation.displayId: - self.assertTrue( - "http://identifiers.org/biomodels.sbo/SBO:0000010" - in participation.roles, - "Restriction reactant participation missing 'reactant' role", - ) - contains_reactant = True - elif "product" in participation.displayId: - self.assertTrue( - "http://identifiers.org/biomodels.sbo/SBO:0000011" - in participation.roles, - "Restriction product participation missing 'product' role", - ) - contains_product = True - - self.assertTrue( - contains_product, "Digestion Assembly plan missing product participation" - ) - self.assertTrue( - contains_reactant, "Digestion Assembly plan missing reactant participation" - ) - self.assertTrue( - contains_restriction, - "Digestion Assembly plan missing restriction participation", - ) - sbol_validation_result = product_doc.validate() self.assertEqual( sbol_validation_result, @@ -201,21 +182,29 @@ def test_backbone_digestion(self): def test_ligation(self): ligation_doc = sbol2.Document() - temp_doc = sbol2.Document() reactants_list = [] - assembly_plan = sbol2.ModuleDefinition("assembly_plan") + assembly_activity = self.assembly.initialize_assembly_activity() parts = [ - "tests/test_files/pro_in_bb.xml", - "tests/test_files/rbs_in_bb.xml", - "tests/test_files/cds_in_bb.xml", - "tests/test_files/terminator_in_bb.xml", + self.source_doc.get( + "https://synbiohub.org/user/Gon/impl_test/pJ23100_AB_impl/1" + ), + self.source_doc.get( + "https://synbiohub.org/user/Gon/impl_test/pB0034_BC_impl/1" + ), + self.source_doc.get( + "https://synbiohub.org/user/Gon/impl_test/pE0030_CD_impl/1" + ), + self.source_doc.get( + "https://synbiohub.org/user/Gon/impl_test/pB0015_DE_impl/1" + ), ] - for i, part in enumerate(parts): - temp_doc.read(part) - md = temp_doc.getModuleDefinition("https://sbolcanvas.org/module1") - extracts_tuple_list, assembly_plan = part_digestion( - md, [rebase_restriction_enzyme("BsaI")], assembly_plan, temp_doc + for i, impl in enumerate(parts): + definition = self.source_doc.get(impl.built) + plasmid = Plasmid(definition, None, [impl], None, self.source_doc) + + extracts_tuple_list, assembly_activity = part_digestion( + plasmid, [self.re_impl], assembly_activity, self.source_doc ) for extract, sequence in extracts_tuple_list: @@ -230,11 +219,22 @@ def test_ligation(self): reactants_list.append(extracts_tuple_list[0][0]) - temp_doc.read("tests/test_files/backbone.xml") + backbone_impl = self.source_doc.get( + "https://synbiohub.org/user/Gon/impl_test/DVK_AE_impl/1" + ) + # run digestion, extract component + sequence, add to ligation_doc, reactants_list - md = temp_doc.getModuleDefinition("https://sbolcanvas.org/module1") - extracts_tuple_list, assembly_plan = backbone_digestion( - md, [rebase_restriction_enzyme("BsaI")], assembly_plan, temp_doc + definition = self.source_doc.get(backbone_impl.built) + + self.sbh.pull( + "https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_AE/1", + self.source_doc, + ) + + plasmid = Plasmid(definition, None, [backbone_impl], None, self.source_doc) + + extracts_tuple_list, assembly_activity = backbone_digestion( + plasmid, [self.re_impl], assembly_activity, self.source_doc ) for extract, seq in extracts_tuple_list: try: @@ -248,91 +248,97 @@ def test_ligation(self): else: print(e) - ligation_doc.add(assembly_plan) + ligation_doc.add(assembly_activity) reactants_list.append(extracts_tuple_list[0][0]) - ligation_doc.add(rebase_restriction_enzyme("BsaI")) + ligation_doc.add_list([self.re_impl, self.ligase_impl]) + self.sbh.pull(self.ligase_impl.built, ligation_doc) - pl = ligation(reactants_list, assembly_plan, ligation_doc) + final_doc = sbol2.Document() - for p in pl: - for obj in p: - ligation_doc.add(obj) + composite_impls = ligation( + reactants_list, + assembly_activity, + "test", + ligation_doc, + final_doc, + self.ligase_impl, + ) - if type(obj) is sbol2.ComponentDefinition: - self.assertTrue( - "http://identifiers.org/so/SO:0000988" in obj.types, - "Ligation product missing circular DNA type", - ) - self.assertTrue( - "http://www.biopax.org/release/biopax-level3.owl#Dna" - in obj.types, - "Ligation product missing DNA Molecule type", - ) - self.assertTrue( - "http://identifiers.org/so/SO:0000804" in obj.roles, - "Ligation product missing engineered region role", - ) + for i in composite_impls: + obj = final_doc.get(i.built) + + if type(obj) is sbol2.ComponentDefinition: + self.assertTrue( + CIRCULAR in obj.types, + "Ligation product missing circular DNA type", + ) + self.assertTrue( + "http://www.biopax.org/release/biopax-level3.owl#Dna" in obj.types, + "Ligation product missing DNA Molecule type", + ) + self.assertTrue( + ENGINEERED_PLASMID in obj.roles, + "Ligation product missing engineered plasmid role", + ) - locations = [] + locations = [] - for anno in obj.sequenceAnnotations: - for location in anno.locations: - locations.append( - (anno.identity, location.start, location.end) - ) + for anno in obj.sequenceAnnotations: + for location in anno.locations: + locations.append((anno.identity, location.start, location.end)) - locations.sort(key=lambda x: x[1]) + locations.sort(key=lambda x: x[1]) - for i in range(len(locations) - 1): - current_end = locations[i][2] - next_start = locations[i + 1][1] + for i in range(len(locations) - 1): + current_end = locations[i][2] + next_start = locations[i + 1][1] - self.assertEqual( - current_end + 1, - next_start, - f"Mismatch in continuity: {locations[i][0]} ends at {current_end}, " - f"but {locations[i + 1][0]} starts at {next_start}", - ) + self.assertEqual( + current_end + 1, + next_start, + f"Mismatch in continuity: {locations[i][0]} ends at {current_end}, " + f"but {locations[i + 1][0]} starts at {next_start}", + ) - sbol_validation_result = ligation_doc.validate() + sbol_validation_result = final_doc.validate() self.assertEqual( sbol_validation_result, "Valid.", "Ligation SBOL validation failed" ) - def test_golden_gate(self): - pro_doc = sbol2.Document() - pro_doc.read("tests/test_files/pro_in_bb.xml") + # def test_golden_gate(self): + # pro_doc = sbol2.Document() + # pro_doc.read("tests/test_files/pro_in_bb.xml") - rbs_doc = sbol2.Document() - rbs_doc.read("tests/test_files/rbs_in_bb.xml") + # rbs_doc = sbol2.Document() + # rbs_doc.read("tests/test_files/rbs_in_bb.xml") - cds_doc = sbol2.Document() - cds_doc.read("tests/test_files/cds_in_bb.xml") + # cds_doc = sbol2.Document() + # cds_doc.read("tests/test_files/cds_in_bb.xml") - ter_doc = sbol2.Document() - ter_doc.read("tests/test_files/terminator_in_bb.xml") + # ter_doc = sbol2.Document() + # ter_doc.read("tests/test_files/terminator_in_bb.xml") - bb_doc = sbol2.Document() - bb_doc.read("tests/test_files/backbone.xml") + # bb_doc = sbol2.Document() + # bb_doc.read("tests/test_files/backbone.xml") - part_docs = [pro_doc, rbs_doc, cds_doc, ter_doc] + # part_docs = [pro_doc, rbs_doc, cds_doc, ter_doc] - assembly_doc = sbol2.Document() - assembly_obj = golden_gate_assembly_plan( - "testassem", part_docs, bb_doc, "BsaI", assembly_doc - ) + # assembly_doc = sbol2.Document() + # assembly_obj = golden_gate_assembly_plan( + # "testassem", part_docs, bb_doc, "BsaI", assembly_doc + # ) - composites = assembly_obj.run(plasmids_in_module_definitions=True) + # composites = assembly_obj.run(plasmids_in_module_definitions=True) - self.assertEqual(len(composites), 1) + # self.assertEqual(len(composites), 1) - assembly_doc.write("validation_assembly.xml") + # assembly_doc.write("validation_assembly.xml") - sbol_validation_result = assembly_doc.validate() - self.assertEqual( - sbol_validation_result, "Valid.", "Assembly SBOL validation failed" - ) + # sbol_validation_result = assembly_doc.validate() + # self.assertEqual( + # sbol_validation_result, "Valid.", "Assembly SBOL validation failed" + # ) if __name__ == "__main__": From e62294131eeb8ce9e76c2ac8352132b4f7296954 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Thu, 7 May 2026 15:32:47 -0600 Subject: [PATCH 66/93] combinatorial test --- notebooks/comb_design.ipynb | 567 ++++++++++++++++-------------------- 1 file changed, 255 insertions(+), 312 deletions(-) diff --git a/notebooks/comb_design.ipynb b/notebooks/comb_design.ipynb index ead09ff..493be94 100644 --- a/notebooks/comb_design.ipynb +++ b/notebooks/comb_design.ipynb @@ -8,8 +8,7 @@ "outputs": [], "source": [ "import sbol2\n", - "from typing import Dict, List\n", - "from sbol2build.abstract_translator import extract_design_parts" + "from buildcompiler.buildcompiler import BuildCompiler" ] }, { @@ -22,386 +21,330 @@ "name": "stdout", "output_type": "stream", "text": [ - "Design........................0\n", - "Build.........................0\n", - "Test..........................0\n", - "Analysis......................0\n", - "ComponentDefinition...........5\n", - "ModuleDefinition..............0\n", - "Model.........................0\n", - "Sequence......................4\n", - "Collection....................0\n", - "Activity......................0\n", - "Plan..........................0\n", - "Agent.........................0\n", - "Attachment....................0\n", - "CombinatorialDerivation.......1\n", - "Implementation................0\n", - "SampleRoster..................0\n", - "Experiment....................0\n", - "ExperimentalData..............0\n", - "Annotation Objects............5\n", - "---\n", - "Total: .........................15\n", - "\n" + "\n" ] } ], "source": [ "abstract_doc = sbol2.Document()\n", "abstract_doc.read(\"tests/test_files/combinatorial_1.xml\")\n", - "print(abstract_doc)" + "comb_design = abstract_doc.combinatorialderivations[0]\n", + "print(type(comb_design))" ] }, { "cell_type": "code", "execution_count": 3, - "id": "f0fb25d5", + "id": "39f37870", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "https://sbolcanvas.org/abstract_combinatorial/1\n" + "Indexing collection: https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\n", + "Indexing collection: https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\n" ] } ], "source": [ - "toplevel = abstract_doc.componentDefinitions[\n", - " 2\n", - "] # TODO develop approach to extract toplevel definition in non MD documents; maybe check for annotations or subcomponents\n", - "print(toplevel)" + "auth = \"47c58a25-78d5-4aee-b74b-9cd2e25da55e\"\n", + "collections = [\n", + " \"https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\",\n", + " \"https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\",\n", + "]\n", + "buildcompiler = BuildCompiler(collections, \"https://synbiohub.org\", auth, abstract_doc)" ] }, { "cell_type": "code", "execution_count": 4, - "id": "c36c9c45", + "id": "c3bbf89a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "matched pJ23100_AB_A_B with DVK_AE_A_E on fusion site A!\n", + "matched pB0033_BC_B_C with pJ23100_AB_A_B on fusion site B!\n", + "matched pE0040_CD_C_D with pB0033_BC_B_C on fusion site C!\n", + "matched final component pB0015_DE_D_E with pE0040_CD_C_D and DVK_AE_A_E on fusion sites (D, E)!\n", + "Success with backbone: DVK_AE_A_E and plasmids: ['pJ23100_AB_A_B', 'pB0033_BC_B_C', 'pE0040_CD_C_D', 'pB0015_DE_D_E']\n", + "matched pJ23100_AB_A_B with DVK_AE_A_E on fusion site A!\n", + "matched pB0032_BC_B_C with pJ23100_AB_A_B on fusion site B!\n", + "matched pE0040_CD_C_D with pB0032_BC_B_C on fusion site C!\n", + "matched final component pB0015_DE_D_E with pE0040_CD_C_D and DVK_AE_A_E on fusion sites (D, E)!\n" + ] + } + ], "source": [ - "parts = extract_design_parts(toplevel, abstract_doc)" + "list = buildcompiler.assembly_lvl1(comb_design)" ] }, { "cell_type": "code", "execution_count": 5, - "id": "bf3932d9", + "id": "3d70b565", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1\n", - "https://sbolcanvas.org/RBS_HFQr/1\n", - "https://synbiohub.org/user/Gon/CIDARMoCloParts/E0040m_gfp/1\n", - "https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1\n" + "({'https://sbolcanvas.org/abstract_combinatorial/1': [Plasmid:\n", + " Name: abstract_combinatorial_composite_comb0_1_A_E\n", + " Plasmid Definition: https://SBOL2Build.org/abstract_combinatorial_composite_comb0_1/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://SBOL2Build.org/abstract_combinatorial_composite_comb0_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['A', 'E']\n", + " Antibiotic Resistance: Kanamycin\n", + ", Plasmid:\n", + " Name: abstract_combinatorial_composite_comb1_1_A_E\n", + " Plasmid Definition: https://SBOL2Build.org/abstract_combinatorial_composite_comb1_1/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://SBOL2Build.org/abstract_combinatorial_composite_comb1_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['A', 'E']\n", + " Antibiotic Resistance: Kanamycin\n", + "]}, )\n" ] } ], "source": [ - "for part in parts:\n", - " print(part)" + "print(list)" ] }, { "cell_type": "code", "execution_count": 6, - "id": "ecd909f4", - "metadata": {}, - "outputs": [], - "source": [ - "def extract_combinatorial_design_parts(\n", - " design: sbol2.ComponentDefinition, doc: sbol2.Document, plasmid_doc\n", - ") -> Dict[str, List[sbol2.ComponentDefinition]]:\n", - " \"\"\"\n", - " Extracts and returns a mapping of component definitions from a combinatorial design, in order.\n", - "\n", - " Retrieves the components in sequential order from the given design.\n", - " Variants of combinatinatorial components are entered in a list corresponding to the URI of the component in the abstract design.\n", - "\n", - " Args:\n", - " design:\n", - " The :class:`sbol2.ComponentDefinition` representing the top-level design\n", - " from which to extract parts.\n", - " doc:\n", - " The primary :class:`sbol2.Document` containing the base component definitions\n", - " and combinatorial derivations.\n", - " plasmid_doc:\n", - " An additional :class:`sbol2.Document` used to resolve component variants\n", - " (plasmid-specific variants referenced by combinatorial derivations).\n", - "\n", - " Returns:\n", - " Dict[str, List[sbol2.ComponentDefinition]]:\n", - " A dictionary mapping component identities to lists\n", - " of corresponding component definitions.\n", - "\n", - " - Sequential design components map to lists containing a single definition.\n", - " - Combinatorial variable components map to lists of variant definitions.\n", - " \"\"\"\n", - " component_list = [c for c in design.getInSequentialOrder()]\n", - " component_dict = {\n", - " component.identity: [doc.getComponentDefinition(component.definition)]\n", - " for component in component_list\n", - " }\n", - "\n", - " for deriv in doc.combinatorialderivations:\n", - " for component in deriv.variableComponents:\n", - " component_dict[component.variable] = [\n", - " plasmid_doc.getComponentDefinition(var) for var in component.variants\n", - " ]\n", - "\n", - " return component_dict" - ] - }, - { - "cell_type": "markdown", - "id": "0ee4be8f", - "metadata": {}, - "source": [ - "### Problem: variant definitions are not included in sbolCanvas abstract combinatorial design document, only references\n", - "#### Solution: references can be resolved by pulling from plasmid collection" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "4fef4173", - "metadata": {}, - "outputs": [], - "source": [ - "plasmid_collection = sbol2.Document()\n", - "sbh = sbol2.PartShop(\"https://synbiohub.org\")\n", - "sbh.pull(\n", - " \"https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/CIDARMoCloPlasmidsKit_collection/1/b7fdc21c6601a61d3166073a9e50f2c3843e1df5/share\",\n", - " plasmid_collection,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "5d87a7fd", + "id": "99b474f2", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "https://sbolcanvas.org/abstract_combinatorial/J23100_1/1:\n", - "J23100\n", - "https://sbolcanvas.org/abstract_combinatorial/RBS_HFQr_2/1:\n", - "B0032\n", - "B0033\n", - "https://sbolcanvas.org/abstract_combinatorial/E0040m_gfp_3/1:\n", - "E0040m_gfp\n", - "https://sbolcanvas.org/abstract_combinatorial/B0015_4/1:\n", - "B0015\n" + "[Plasmid:\n", + " Name: pE0040_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0040_CD/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pE0040_CD_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['C', 'D']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0034_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0034_BC/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0034_BC_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['B', 'C']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_EB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23100_EB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['E', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_AB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23100_AB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0033_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0033_BC/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0033_BC_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['B', 'C']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23106_FB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_EB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23116_EB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['E', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_GB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23116_GB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pE0030_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0030_CD/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pE0030_CD_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['C', 'D']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23100_FB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_AB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23116_AB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0032_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0032_BC/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0032_BC_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['B', 'C']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DF_D_F\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DF/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0015_DF_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'F']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DG_D_G\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DG/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0015_DG_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'G']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DH_D_H\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DH/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0015_DH_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'H']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_AB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23106_AB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_GB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23106_GB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_GB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23100_GB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pE1010_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE1010_CD/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pE1010_CD_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['C', 'D']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DE_D_E\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DE/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0015_DE_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'E']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23116_FB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_EB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23106_EB_impl/1']\n", + " Strain Implementations: None\n", + " Fusion Sites: ['E', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: abstract_combinatorial_composite_comb0_1_A_E\n", + " Plasmid Definition: https://SBOL2Build.org/abstract_combinatorial_composite_comb0_1/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://SBOL2Build.org/abstract_combinatorial_composite_comb0_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['A', 'E']\n", + " Antibiotic Resistance: Kanamycin\n", + ", Plasmid:\n", + " Name: abstract_combinatorial_composite_comb1_1_A_E\n", + " Plasmid Definition: https://SBOL2Build.org/abstract_combinatorial_composite_comb1_1/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['https://SBOL2Build.org/abstract_combinatorial_composite_comb1_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['A', 'E']\n", + " Antibiotic Resistance: Kanamycin\n", + "]\n" ] } ], "source": [ - "dict = extract_combinatorial_design_parts(toplevel, abstract_doc, plasmid_collection)\n", - "\n", - "for key, value in dict.items():\n", - " print(f\"{key}:\")\n", - " for element in value:\n", - " print(element.displayId)" + "print(buildcompiler.indexed_plasmids)" ] }, { - "cell_type": "code", - "execution_count": 9, - "id": "59be7c31", - "metadata": {}, - "outputs": [], - "source": [ - "import itertools\n", - "\n", - "\n", - "def enumerate_design_variants(component_dict):\n", - " \"\"\"\n", - " Given a dict mapping variable component identities to lists of ComponentDefinitions,\n", - " generate all possible design combinations as lists of ComponentDefinitions\n", - " (in consistent order of keys).\n", - " \"\"\"\n", - " keys = list(component_dict.keys())\n", - " variant_lists = [component_dict[k] for k in keys]\n", - "\n", - " # Cartesian product across all variant lists\n", - " all_variants = list(itertools.product(*variant_lists))\n", - "\n", - " # Convert tuples to lists (optional) for clarity\n", - " all_variants = [list(combo) for combo in all_variants]\n", - "\n", - " return all_variants" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "f2c774c9", - "metadata": {}, - "outputs": [], - "source": [ - "from sbol2build.abstract_translator import (\n", - " construct_plasmid_dict,\n", - " extract_toplevel_definition,\n", - " get_compatible_plasmids,\n", - " MocloPlasmid,\n", - ")\n", - "\n", - "enumerated = enumerate_design_variants(dict)\n", - "\n", - "plasmid_dicts = []\n", - "\n", - "# for design in enumerated:\n", - "# plasmid_dicts.append(construct_plasmid_dict(design, plasmid_collection))" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "0470f3f9", + "cell_type": "markdown", + "id": "0ee4be8f", "metadata": {}, - "outputs": [], "source": [ - "def translate_abstract_to_plasmids(\n", - " abstract_design_doc: sbol2.Document,\n", - " plasmid_collection: sbol2.Document,\n", - " backbone_doc: sbol2.Document,\n", - "):\n", - " backbone_def = extract_toplevel_definition(backbone_doc)\n", - " backbone_plasmid = MocloPlasmid(backbone_def.displayId, backbone_def, backbone_doc)\n", - "\n", - " # combinatorial design\n", - " if len(abstract_design_doc.combinatorialderivations) > 0:\n", - " abstract_design_def = abstract_design_doc.getComponentDefinition(\n", - " abstract_design_doc.combinatorialderivations[0].masterTemplate\n", - " )\n", - "\n", - " combinatorial_part_dict = extract_combinatorial_design_parts(\n", - " abstract_design_def, abstract_design_doc, plasmid_collection\n", - " )\n", - " enumerated_part_list = enumerate_design_variants(combinatorial_part_dict)\n", - "\n", - " final_plasmid_list = []\n", - "\n", - " for design in enumerated_part_list:\n", - " plasmid_dict = construct_plasmid_dict(design, plasmid_collection)\n", - " final_plasmid_list += get_compatible_plasmids(\n", - " plasmid_dict, backbone_plasmid\n", - " )\n", - "\n", - " return set(final_plasmid_list)\n", - "\n", - " # generic design\n", - " else:\n", - " abstract_design_def = extract_toplevel_definition(abstract_design_doc)\n", - "\n", - " ordered_part_definitions = extract_design_parts(\n", - " abstract_design_def, abstract_design_doc\n", - " )\n", - "\n", - " plasmid_dict = construct_plasmid_dict(\n", - " ordered_part_definitions, plasmid_collection\n", - " )\n", - "\n", - " return get_compatible_plasmids(plasmid_dict, backbone_plasmid)" + "### Problem: variant definitions are not included in sbolCanvas abstract combinatorial design document, only references\n", + "#### Solution: references can be resolved by pulling from plasmid collection" ] }, { - "cell_type": "code", - "execution_count": 19, - "id": "97ee6712", + "cell_type": "markdown", + "id": "b592c69c", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_AB/1 with ['Fusion_Site_B', 'Fusion_Site_A']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_GB/1 with ['Fusion_Site_B', 'Fusion_Site_G']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_FB/1 with ['Fusion_Site_F', 'Fusion_Site_B']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_EB/1 with ['Fusion_Site_B', 'Fusion_Site_E']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0032/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0032_BC/1 with ['Fusion_Site_C', 'Fusion_Site_B']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/E0040m_gfp/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0040_CD/1 with ['Fusion_Site_D', 'Fusion_Site_C']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DG/1 with ['Fusion_Site_G', 'Fusion_Site_D']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DF/1 with ['Fusion_Site_F', 'Fusion_Site_D']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DE/1 with ['Fusion_Site_D', 'Fusion_Site_E']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DH/1 with ['Fusion_Site_D', 'Fusion_Site_H']\n", - "matched J23100_A_B with DVK_AE_A_E on fusion site A!\n", - "matched B0032_B_C with J23100_A_B on fusion site B!\n", - "matched E0040m_gfp_C_D with B0032_B_C on fusion site C!\n", - "matched final component B0015_D_E with E0040m_gfp_C_D and DVK_AE_A_E on fusion sites (D, E)!\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_AB/1 with ['Fusion_Site_B', 'Fusion_Site_A']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_GB/1 with ['Fusion_Site_B', 'Fusion_Site_G']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_FB/1 with ['Fusion_Site_F', 'Fusion_Site_B']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/J23100/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_EB/1 with ['Fusion_Site_B', 'Fusion_Site_E']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0033/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0033_BC/1 with ['Fusion_Site_B', 'Fusion_Site_C']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/E0040m_gfp/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0040_CD/1 with ['Fusion_Site_D', 'Fusion_Site_C']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DG/1 with ['Fusion_Site_G', 'Fusion_Site_D']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DF/1 with ['Fusion_Site_F', 'Fusion_Site_D']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DE/1 with ['Fusion_Site_D', 'Fusion_Site_E']\n", - "found: https://synbiohub.org/user/Gon/CIDARMoCloParts/B0015/1 in https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DH/1 with ['Fusion_Site_D', 'Fusion_Site_H']\n", - "matched J23100_A_B with DVK_AE_A_E on fusion site A!\n", - "matched B0033_B_C with J23100_A_B on fusion site B!\n", - "matched E0040m_gfp_C_D with B0033_B_C on fusion site C!\n", - "matched final component B0015_D_E with E0040m_gfp_C_D and DVK_AE_A_E on fusion sites (D, E)!\n" - ] - }, - { - "data": { - "text/plain": [ - "{MocloPlasmid:\n", - " Name: B0015_D_E\n", - " Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DE/1\n", - " Fusion Sites: ['D', 'E'],\n", - " MocloPlasmid:\n", - " Name: B0032_B_C\n", - " Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0032_BC/1\n", - " Fusion Sites: ['B', 'C'],\n", - " MocloPlasmid:\n", - " Name: B0033_B_C\n", - " Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0033_BC/1\n", - " Fusion Sites: ['B', 'C'],\n", - " MocloPlasmid:\n", - " Name: E0040m_gfp_C_D\n", - " Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0040_CD/1\n", - " Fusion Sites: ['C', 'D'],\n", - " MocloPlasmid:\n", - " Name: J23100_A_B\n", - " Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_AB/1\n", - " Fusion Sites: ['A', 'B']}" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "bb_doc = sbol2.Document()\n", - "sbh.pull(\n", - " \"https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_AE/1/647c5b2458567dcce6b0a37178d352b8ffa9a7fe/share\",\n", - " bb_doc,\n", - ")\n", - "\n", - "translate_abstract_to_plasmids(\n", - " abstract_design_doc=abstract_doc,\n", - " plasmid_collection=plasmid_collection,\n", - " backbone_doc=bb_doc,\n", - ")" - ] + "source": [] }, { - "cell_type": "code", - "execution_count": null, - "id": "06f2668c", + "cell_type": "markdown", + "id": "42a819eb", "metadata": {}, - "outputs": [], "source": [] } ], From 4e704fcaf34a9f2e8c17acee42d726dad35a94a0 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Fri, 8 May 2026 11:45:25 -0600 Subject: [PATCH 67/93] type and error handling adjustments --- src/buildcompiler/sbol2build.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/buildcompiler/sbol2build.py b/src/buildcompiler/sbol2build.py index 1de23c9..4ad1312 100644 --- a/src/buildcompiler/sbol2build.py +++ b/src/buildcompiler/sbol2build.py @@ -20,7 +20,7 @@ THREE_PRIME_OVERHANG, ) -sbol2.Config.setHomespace("https://SBOL2Build.org") +sbol2.Config.setHomespace("http://buildcompiler.org") sbol2.Config.setOption(sbol2.ConfigOptions.SBOL_COMPLIANT_URIS, True) sbol2.Config.setOption(sbol2.ConfigOptions.SBOL_TYPED_URIS, False) @@ -99,9 +99,8 @@ def run( append_extracts_to_doc(extracts_tuple_list, self.final_document) self.extracted_parts.append(extracts_tuple_list[0][0]) - backbone_impl = self.backbone.plasmid_implementations[0] extracts_tuple_list, _ = backbone_digestion( - backbone_impl, + self.backbone, [self.restriction_enzyme], self.assembly_activity, self.source_document, @@ -343,7 +342,7 @@ def part_digestion( restriction_enzymes: List[sbol2.Implementation], assembly_activity: sbol2.Activity, document: sbol2.Document, -) -> Tuple[List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]]]: +) -> Tuple[List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]], sbol2.Activity]: """Simulate restriction digestion of a part plasmid and extract the insert. Uses PyDNA to cut the reactant sequence, then constructs SBOL representations @@ -574,11 +573,11 @@ def part_digestion( def backbone_digestion( - reactant: sbol2.Implementation, + reactant: Plasmid, restriction_enzymes: List[sbol2.Implementation], assembly_activity: sbol2.Activity, document: sbol2.Document, -) -> Tuple[List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]]]: +) -> Tuple[List[Tuple[sbol2.ComponentDefinition, sbol2.Sequence]], sbol2.Activity]: """Simulate restriction digestion of a backbone plasmid and extract the linearised vector. Mirrors :func:`part_digestion` but targets the backbone: for a circular reactant @@ -601,7 +600,8 @@ def backbone_digestion( :raises ValueError: If the reactant does not have exactly one sequence, or if the number of digest products is unsupported for the reactant topology. """ - reactant_component_definition = document.get(reactant.built) + reactant_impl = reactant.plasmid_implementations[0] + reactant_component_definition = document.get(reactant_impl.built) reactant_displayId = reactant_component_definition.displayId types = set(reactant_component_definition.types or []) @@ -619,8 +619,8 @@ def backbone_digestion( assembly_activity.usages.add( sbol2.Usage( - uri=f"{reactant.displayId}", - entity=reactant.identity, + uri=f"{reactant_impl.displayId}", + entity=reactant_impl.identity, role="http://sbols.org/v2#build", ) ) @@ -665,7 +665,7 @@ def backbone_digestion( if len(digested_reactant) < 2 or len(digested_reactant) > 3: raise ValueError( - f"Not supported number of products. Found: {len(digested_reactant)}" + f"Not supported number of products. Found: {len(digested_reactant)} after digesting {reactant_displayId}" ) # TODO select them based on content rather than size. elif circular and len(digested_reactant) == 2: From a0bb9e0058470c07af988ca81e38caa05b5ec5e7 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Fri, 8 May 2026 11:47:19 -0600 Subject: [PATCH 68/93] adjusting backbone arg to allow for dictionary mapping design.displayId:backbone in multi-design assemblylvl1 --- src/buildcompiler/buildcompiler.py | 54 +++++++++++++++++++----------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index c8b13b7..b31c64f 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -13,7 +13,6 @@ from .abstract_translator import ( get_or_pull, get_compatible_plasmids, - extract_toplevel_definition, ) from .constants import ( AMP, @@ -292,7 +291,7 @@ def assembly_lvl1( | sbol2.CombinatorialDerivation, final_doc: sbol2.Document = sbol2.Document(), product_name: str = "composite", - backbone: Plasmid = None, + backbone: Plasmid | Dict[str, Plasmid] | None = None, ) -> Tuple[Dict, sbol2.Document]: """Assemble level-1 plasmids for each gene/transcriptional unit. @@ -317,13 +316,18 @@ def assembly_lvl1( enumerated_part_lists = enumerate_design_variants(combinatorial_part_dict) for i, list in enumerate(enumerated_part_lists): - plasmid_dict = self._construct_plasmid_dict(list, "Ampicillin") + plasmid_dict = self._construct_plasmid_dict(list, AMP) - if not backbone: + if isinstance(backbone, dict): + raise ValueError( + "A backbone dictionary cannot be used with a CombinatorialDerivation. " + "All variants share the same template, so supply a single Plasmid or None to auto-select." + ) + elif not backbone: backbone, compatible_plasmids = self._get_backbone( plasmid_dict, antibiotic_resistance=KAN ) - else: + elif type(backbone) is Plasmid: compatible_plasmids = get_compatible_plasmids( plasmid_dict, backbone ) @@ -362,19 +366,30 @@ def assembly_lvl1( assembly_dict.setdefault(abstract_design_def.identity, []).extend( composite_plasmids ) - else: + else: # list of designs for abstract_design in abstract_designs: plasmid_dict = self._get_input_plasmids( - design=abstract_designs, antibiotic_resistance=AMP + design=abstract_design, antibiotic_resistance=AMP ) if not backbone: - backbone, compatible_plasmids = self._get_backbone( + resolved_backbone, compatible_plasmids = self._get_backbone( plasmid_dict, antibiotic_resistance=KAN ) - else: + elif isinstance(backbone, dict): + resolved_backbone = backbone.get(abstract_design.displayId) + if resolved_backbone is None: + raise ValueError( + f"Backbone dict provided but no entry found for design '{abstract_design.displayId}'. " + f"Available keys: {list(backbone.keys())}" + ) compatible_plasmids = get_compatible_plasmids( - plasmid_dict, backbone + plasmid_dict, resolved_backbone + ) + else: + resolved_backbone, compatible_plasmids = ( + backbone, + get_compatible_plasmids(plasmid_dict, backbone), ) if self.BsaI_impl is None: @@ -393,7 +408,7 @@ def assembly_lvl1( assembly = Assembly( compatible_plasmids, - backbone, + resolved_backbone, self.BsaI_impl, self.T4_ligase_impl, self.sbol_doc, @@ -438,6 +453,7 @@ def assembly_lvl2( # send original abstract_design to get a new dictionary # send new dictionary to _get_backbone or get_compatible plasmids with AMP TUs = _extract_lvl2_TUs(abstract_design_doc) + backbone_dict = {} lvl1_plasmids = [] for i, TU in enumerate(TUs): @@ -452,16 +468,16 @@ def assembly_lvl2( and plasmid.antibiotic_resistance == KAN ) - print(backbone) + backbone_dict[TU.displayId] = backbone # TODO insert check here to see if the TU exists already (#43). should not be too expensive, as long as we search only indexed_plasmids where AR=KAN - composite_plasmids, final_doc = self.assembly_lvl1( - TU, backbone=backbone, product_name=f"{TU.displayId}_plas" - ) - simplified_representation, new_defs = self._encapsulate_TU( - composite_plasmids[0] - ) + composite_plasmid_dict, final_doc = self.assembly_lvl1( + TUs, backbone=backbone_dict, product_name=f"{TU.displayId}_plas" + ) + + for key, composites in composite_plasmid_dict.items(): + simplified_representation, new_defs = self._encapsulate_TU(composites[0]) final_doc.add_list(new_defs) lvl1_plasmids.append(simplified_representation) print(simplified_representation) @@ -1091,7 +1107,7 @@ def _extract_lvl2_TUs( # TODO send to misc helper file instead of buildcompiler Returns: A list of TU component definitions in sequential order. """ - top_design = extract_toplevel_definition(design_doc) + top_design = design_doc.componentDefinitions[0] return [ design_doc.get(comp.definition) for comp in top_design.getInSequentialOrder() From cc967fefa83e1d09a184913cc1a26733af2fccf4 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Fri, 8 May 2026 13:20:58 -0600 Subject: [PATCH 69/93] type correction --- src/buildcompiler/plasmid.py | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/src/buildcompiler/plasmid.py b/src/buildcompiler/plasmid.py index 57fada4..3907cc1 100644 --- a/src/buildcompiler/plasmid.py +++ b/src/buildcompiler/plasmid.py @@ -12,8 +12,8 @@ def __init__( self, definition: sbol2.ComponentDefinition, strain_definition: sbol2.ModuleDefinition, - plasmid_implementations: sbol2.Implementation, - strain_implementations: sbol2.Implementation, + plasmid_implementations: List[sbol2.Implementation], + strain_implementations: List[sbol2.Implementation], doc: sbol2.document, ): self.plasmid_definition = definition @@ -93,26 +93,3 @@ def __eq__(self, other): def __hash__(self): return hash(self.plasmid_definition) - - -# def _extract_fusion_sites( -# plasmid: sbol2.ComponentDefinition, -# doc: sbol2.Document, -# sbh: sbol2.PartShop -# ) -> List[sbol2.ComponentDefinition]: -# """ -# Returns all fusion site component definitions from a plasmid. - -# Args: -# plasmid: :class:`sbol2.ComponentDefinition` representing the plasmid. - -# Returns: -# A list of fusion site component definitions. -# """ -# fusion_sites = [] -# for component in plasmid.components: -# definition = get_or_pull(doc, sbh, component.definition) -# if RESTRICTION_ENZYME_ASSEMBLY_SCAR in definition.roles: -# fusion_sites.append(definition) - -# return fusion_sites From b4fb2cae2d1e39269405b5f6d6ffe7cad42c31cf Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Fri, 8 May 2026 14:03:39 -0600 Subject: [PATCH 70/93] Add local SBOL document indexing entry point --- src/buildcompiler/buildcompiler.py | 64 +++++++++++++++++++++++++++--- tests/test_buildcompiler.py | 58 +++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 tests/test_buildcompiler.py diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index c1e646a..57b4b58 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -76,6 +76,33 @@ def __init__( self._index_collections(collections) + @classmethod + def from_local_documents( + cls, + collection_docs: list[sbol2.Document], + design_doc: sbol2.Document | None = None, + ): + """Create a BuildCompiler instance from already-loaded local SBOL documents.""" + compiler = cls.__new__(cls) + compiler.sbh = None + compiler.sbol_doc = sbol2.Document() + compiler.indexed_plasmids = [] + compiler.indexed_backbones = [] + compiler.restriction_enzyme_implementations = [] + compiler.ligase_implementations = [] + + if design_doc is not None: + compiler.index_document(design_doc) + + for collection_doc in collection_docs: + compiler.index_document(collection_doc) + + return compiler + + def index_document(self, collection_doc: sbol2.Document): + self._merge_document(collection_doc) + self._index_current_document() + def _index_collections(self, collections: List[str]): """Index input collections into plasmids and backbones. @@ -91,9 +118,34 @@ def _index_collections(self, collections: List[str]): for uri in collections: print(f"Indexing collection: {uri}") self.sbh.pull(uri, self.sbol_doc) + self._index_current_document() + + def _merge_document(self, source_doc: sbol2.Document): + try: + self.sbol_doc.appendString(source_doc.writeString()) + except RuntimeError as exc: + if "SBOL_ERROR_URI_NOT_UNIQUE" in str(exc): + for top_level in source_doc.SBOLObjects.values(): + if top_level.identity in self.sbol_doc: + continue + self.sbol_doc.add(top_level.copy()) + else: + raise + + def _resolve_object(self, uri: str): + existing = self.sbol_doc.find(uri) + if existing is not None: + return existing + if self.sbh is None: + raise ValueError( + f"Referenced SBOL object not found in local documents: {uri}. " + "Local mode does not pull from SynBioHub." + ) + return get_or_pull(self.sbol_doc, self.sbh, uri) + def _index_current_document(self): for implementation in self.sbol_doc.implementations: - built_object = get_or_pull(self.sbol_doc, self.sbh, implementation.built) + built_object = self._resolve_object(implementation.built) if ( type(built_object) is sbol2.ModuleDefinition and ORGANISM_STRAIN in built_object.roles @@ -627,7 +679,7 @@ def _extract_plasmids_from_strain( ): # strain_implementation = optional param for plasmid in strain.functionalComponents: - plasmid_definition = get_or_pull(doc, self.sbh, plasmid.definition) + plasmid_definition = self._resolve_object(plasmid.definition) if ENGINEERED_PLASMID in plasmid_definition.roles: existing = self._get_indexed_plasmid( @@ -761,7 +813,7 @@ def _extract_design_parts( """ component_list = [c for c in design.getInSequentialOrder()] return [ - get_or_pull(self.sbol_doc, self.sbh, component.definition) + self._resolve_object(component.definition) for component in component_list ] @@ -775,7 +827,7 @@ def _get_abstract_design(self) -> sbol2.ComponentDefinition: continue component_definitions = [ - get_or_pull(self.sbol_doc, self.sbh, component.definition) + self._resolve_object(component.definition) for component in definition.getInSequentialOrder() ] if any( @@ -837,7 +889,7 @@ def _is_single_part(self, plasmid: sbol2.ComponentDefinition) -> bool: return False else: component_definitions = [ - get_or_pull(self.sbol_doc, self.sbh, comp.definition) + self._resolve_object(comp.definition) for comp in plasmid.getInSequentialOrder() ] @@ -1017,7 +1069,7 @@ def _expand_combinatorial_derivation( derivation: sbol2.CombinatorialDerivation, product_name_prefix: str = None, ) -> list[sbol2.ComponentDefinition]: - master_template = get_or_pull(self.sbol_doc, self.sbh, derivation.masterTemplate) + master_template = self._resolve_object(derivation.masterTemplate) component_variants = extract_combinatorial_design_parts( master_template, self.sbol_doc, self.sbol_doc ) diff --git a/tests/test_buildcompiler.py b/tests/test_buildcompiler.py new file mode 100644 index 0000000..4acd224 --- /dev/null +++ b/tests/test_buildcompiler.py @@ -0,0 +1,58 @@ +import inspect +import os +import sys +import unittest +from unittest.mock import patch + +import sbol2 + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src'))) + +from buildcompiler.buildcompiler import BuildCompiler +from buildcompiler.constants import RESTRICTION_ENZYME + + +class TestBuildCompilerLocalIndexing(unittest.TestCase): + def test_constructor_signature_unchanged(self): + params = list(inspect.signature(BuildCompiler.__init__).parameters.keys()) + self.assertEqual( + params, + ['self', 'collections', 'sbh_registry', 'auth_token', 'sbol_doc'], + ) + + def test_from_local_documents_indexes_without_partshop(self): + collection_doc = sbol2.Document() + enzyme = sbol2.ComponentDefinition('BsaI') + enzyme.types = [sbol2.BIOPAX_PROTEIN] + enzyme.roles = [RESTRICTION_ENZYME] + collection_doc.add(enzyme) + implementation = sbol2.Implementation('BsaI_impl') + implementation.built = enzyme.identity + collection_doc.add(implementation) + + with patch('sbol2.PartShop', side_effect=AssertionError('PartShop should not be constructed in local mode')): + compiler = BuildCompiler.from_local_documents([collection_doc]) + + self.assertIsNone(compiler.sbh) + self.assertEqual(len(compiler.indexed_plasmids), 0) + self.assertEqual(len(compiler.indexed_backbones), 0) + self.assertEqual(len(compiler.restriction_enzyme_implementations), 1) + self.assertIsInstance(compiler.restriction_enzyme_implementations, list) + self.assertIsInstance(compiler.ligase_implementations, list) + + def test_local_mode_raises_when_reference_missing(self): + doc = sbol2.Document() + strain = sbol2.ModuleDefinition('strain_missing_ref') + strain.roles = ['https://identifiers.org/ncit/NCIT:C14419'] + fc = strain.functionalComponents.create('plasmid_ref') + fc.definition = 'https://example.org/missing_plasmid/1' + doc.add(strain) + + with self.assertRaises(ValueError) as ctx: + BuildCompiler.from_local_documents([doc]) + + self.assertIn('Local mode does not pull from SynBioHub', str(ctx.exception)) + + +if __name__ == '__main__': + unittest.main() From 4e9f961e6bb8db2a7ef0900d1b7ab15e4e477e9a Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 12 May 2026 13:00:42 -0600 Subject: [PATCH 71/93] activity tests for digestion and ligation --- README.md | 10 ++- src/buildcompiler/constants.py | 2 +- tests/test_core.py | 113 ++++++++++++++++++++++----------- uv.lock | 2 +- 4 files changed, 86 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index ddaaa1a..962d203 100644 --- a/README.md +++ b/README.md @@ -52,5 +52,13 @@ uv run pre-commit install ``` -#### Running tests: +#### Running tests locally: +Run these bash commands to establish your SynBioHub account for collection access. These are saved in GitHub secrets for the automated test suite. + +`export SBH_USERNAME=your_username` + +`export SBH_PASSWORD=your_password` + +Then run the tests with: + `uv run python -m unittest discover -s tests` diff --git a/src/buildcompiler/constants.py b/src/buildcompiler/constants.py index e455c66..76bc182 100644 --- a/src/buildcompiler/constants.py +++ b/src/buildcompiler/constants.py @@ -26,7 +26,7 @@ LVL2_FUSION_SITE_ORDER = [["A", "E"], ["E", "F"], ["F", "G"], ["G", "H"]] -# TODO CHANGE ALL TO HTTP +# TODO CHANGE ALL TO HTTP (MAKE SURE REFERENCED PARTS ON SBH ARE ALSO HTTP FIRST) ENGINEERED_PLASMID = "http://identifiers.org/so/SO:0000637" ENGINEERED_INSERT = "https://identifiers.org/so/SO:0000915" diff --git a/tests/test_core.py b/tests/test_core.py index 0618626..cc5c51b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -7,8 +7,10 @@ from buildcompiler.constants import ( CIRCULAR, + ENGINEERED_INSERT, ENGINEERED_PLASMID, FIVE_PRIME_OVERHANG, + LINEAR, PLASMID_VECTOR, THREE_PRIME_OVERHANG, ) @@ -85,14 +87,37 @@ def test_part_digestion(self): # TODO test activity relationships product_doc.add(extract) product_doc.add(assembly_activity) + usages = list(assembly_activity.usages) + + # Expect: 1 reactant + at least 1 enzyme + self.assertTrue( + len(usages) >= 2, + "assembly activity should include reactant and enzyme usages", + ) + + entities = [u.entity for u in usages] + + # reactant implementation should be present + self.assertIn( + plasmid.plasmid_implementations[0].identity, + entities, + "Reactant implementation missing from activity usages", + ) + + # restriction enzyme should be present + self.assertIn( + self.re_impl.identity, + entities, + "Restriction enzyme missing from activity usages", + ) + extract = parts_list[0][0] - self.assertEqual( - extract.roles, - ["http://identifiers.org/so/SO:0000915"], + self.assertTrue( + ENGINEERED_INSERT in extract.roles, "Part digestion extracted part missing engineered insert role", ) # engineered insert role self.assertTrue( - "http://identifiers.org/so/SO:0000987" in extract.types, + LINEAR in extract.types, "Part digestion extracted part missing linear DNA type", ) @@ -142,6 +167,30 @@ def test_backbone_digestion(self): product_doc.add(extract) product_doc.add(assembly_activity) + usages = list(assembly_activity.usages) + + # Expect: 1 reactant + at least 1 enzyme + self.assertTrue( + len(usages) >= 2, + "Digestion activity should include reactant and enzyme usages", + ) + + entities = [u.entity for u in usages] + + # reactant implementation should be present + self.assertIn( + plasmid.plasmid_implementations[0].identity, + entities, + "Reactant implementation missing from activity usages", + ) + + # restriction enzyme should be present + self.assertIn( + self.re_impl.identity, + entities, + "Restriction enzyme missing from activity usages", + ) + extract = parts_list[0][0] self.assertEqual( extract.roles, @@ -265,9 +314,31 @@ def test_ligation(self): self.ligase_impl, ) + usages = list(assembly_activity.usages) + entities = [u.entity for u in usages] + + self.assertIn( + self.ligase_impl.identity, + entities, + "Ligase missing from assembly activity usages", + ) + + for part_impl in parts: + self.assertIn( + part_impl.identity, + entities, + f"{part_impl.displayId} missing from assembly activity usages", + ) + for i in composite_impls: obj = final_doc.get(i.built) + self.assertEqual( + i.wasGeneratedBy, + [assembly_activity.identity], + "Composite implementation not linked to assembly activity", + ) + if type(obj) is sbol2.ComponentDefinition: self.assertTrue( CIRCULAR in obj.types, @@ -306,40 +377,6 @@ def test_ligation(self): sbol_validation_result, "Valid.", "Ligation SBOL validation failed" ) - # def test_golden_gate(self): - # pro_doc = sbol2.Document() - # pro_doc.read("tests/test_files/pro_in_bb.xml") - - # rbs_doc = sbol2.Document() - # rbs_doc.read("tests/test_files/rbs_in_bb.xml") - - # cds_doc = sbol2.Document() - # cds_doc.read("tests/test_files/cds_in_bb.xml") - - # ter_doc = sbol2.Document() - # ter_doc.read("tests/test_files/terminator_in_bb.xml") - - # bb_doc = sbol2.Document() - # bb_doc.read("tests/test_files/backbone.xml") - - # part_docs = [pro_doc, rbs_doc, cds_doc, ter_doc] - - # assembly_doc = sbol2.Document() - # assembly_obj = golden_gate_assembly_plan( - # "testassem", part_docs, bb_doc, "BsaI", assembly_doc - # ) - - # composites = assembly_obj.run(plasmids_in_module_definitions=True) - - # self.assertEqual(len(composites), 1) - - # assembly_doc.write("validation_assembly.xml") - - # sbol_validation_result = assembly_doc.validate() - # self.assertEqual( - # sbol_validation_result, "Valid.", "Assembly SBOL validation failed" - # ) - if __name__ == "__main__": unittest.main() diff --git a/uv.lock b/uv.lock index 8309d65..ddf3f2e 100644 --- a/uv.lock +++ b/uv.lock @@ -1993,7 +1993,7 @@ wheels = [ [[package]] name = "sbol2build" -version = "0.0b1" +version = "0.0b3" source = { editable = "." } dependencies = [ { name = "biopython", version = "1.81", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, From 3dc25ee26f3d81aaec93ed031d0c8f0f72159a10 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 12 May 2026 14:30:58 -0600 Subject: [PATCH 72/93] test buildcompiler draft --- tests/test_buildcompiler.py | 203 ++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 tests/test_buildcompiler.py diff --git a/tests/test_buildcompiler.py b/tests/test_buildcompiler.py new file mode 100644 index 0000000..aa55f5d --- /dev/null +++ b/tests/test_buildcompiler.py @@ -0,0 +1,203 @@ +# test 1: test same abstract design with each possible circuit selection, ensure the promoter and terminator shift accordingly + +# test 2: inaccessible part in abstract design -> should throw informative error message + +# test 3: (FUTURE) abstract design with multiple TUs + +import sbol2 +import unittest +import sys +import os + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +from buildcompiler.buildcompiler import BuildCompiler + +from buildcompiler.abstract_translator import extract_toplevel_definition + + +class Test_Abstract_Translation_Functions(unittest.TestCase): + @classmethod + def setUpClass(cls): + username = os.environ.get("SBH_USERNAME") + password = os.environ.get("SBH_PASSWORD") + + if not username or not password: + raise RuntimeError( + "Missing SBH_USERNAME and/or SBH_PASSWORD environment variables" + ) + sbh = sbol2.PartShop("https://synbiohub.org") + sbh.login(username, password) + + auth = sbh.key + + collections = [ + "https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1", + "https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1", + ] + + cls.buildcompiler = BuildCompiler( + collections, "https://synbiohub.org", auth, sbol2.Document() + ) + + def test_simple_lvl1_assembly(self): + abstract_design_doc = sbol2.Document() + abstract_design_doc.read("tests/test_files/moclo_parts_circuit.xml") + + design = extract_toplevel_definition(abstract_design_doc) + product_doc = sbol2.Document() + + dict, product_doc = self.buildcompiler.assembly_lvl1([design], product_doc) + + self.assertEqual( + len(dict), + 1, + "There should be 1 composite resulting from the assembly", + ) + + assembly_activity = product_doc.get( + "https://SBOL2Build.org/qlSBuNBL_composite_assembly/1" + ) + + self.assertEqual( + len(assembly_activity.usages), + 7, + "Assembly should have 7 usages: 5 plasmids, 1 ligase, 1 Restriction Enzyme", + ) + + usage_uris = {u.identity for u in assembly_activity.usages} + + expected_usage_uris = { + "https://SBOL2Build.org/qlSBuNBL_composite_assembly/pJ23100_AB_impl/1", + "https://SBOL2Build.org/qlSBuNBL_composite_assembly/pB0034_BC_impl/1", + "https://SBOL2Build.org/qlSBuNBL_composite_assembly/pE0030_CD_impl/1", + "https://SBOL2Build.org/qlSBuNBL_composite_assembly/pB0015_DE_impl/1", + "https://SBOL2Build.org/qlSBuNBL_composite_assembly/DVK_AE_impl/1", + "https://SBOL2Build.org/qlSBuNBL_composite_assembly/BsaI_enzyme/1", + "https://SBOL2Build.org/qlSBuNBL_composite_assembly/T4_Ligase/1", + } + + for expected_uri in expected_usage_uris: + usage = product_doc.get(expected_uri) + impl = product_doc.get(usage.entity) + + self.assertIn( + expected_uri, + usage_uris, + f"Expected usage {expected_uri} was not found in assembly activity usages", + ) + + self.assertIsNotNone( + impl, + f"Entity {impl} should exist in the activity", + ) + + expected_product_uri = "https://SBOL2Build.org/qlSBuNBL_composite_1_impl/1" + + product_impl = product_doc.get(expected_product_uri) + product_def = product_doc.get(product_impl.built) + + self.assertIsNotNone( + product_impl, + f"Implementation {product_impl} should exist in the document", + ) + + self.assertEqual( + product_impl.wasGeneratedBy, + assembly_activity.identity, + f"Implementation {product_impl} must include wasGeneratedBy {assembly_activity.identity}", + ) + + self.assertEqual( + product_def, + "https://SBOL2Build.org/qlSBuNBL_composite_1/1", + f"Implementation {product_impl} must build {product_def}", + ) + + product_doc.write("test_simple_lvl1_assembly.xml") + + # def test_two_rbs_combinatorial_translation(self): + # comb_doc = sbol2.Document() + # comb_doc.read("tests/test_files/combinatorial_1.xml") + + # design = extract_toplevel_definition(comb_doc) + + # self.assertEqual( + # len(comb_plasmid_list), + # 5, + # "There should be 5 plasmids in the abstract translation", + # ) + + # # Run through sbol2build to test composite count + # part_documents = [] + + # for mocloPlasmid in comb_plasmid_list: + # temp_doc = sbol2.Document() + # mocloPlasmid.definition.copy(temp_doc) + # copy_sequences(mocloPlasmid.definition, temp_doc, self.plasmid_collection) + # part_documents.append(temp_doc) + + # assembly_doc = sbol2.Document() + # assembly_obj = golden_gate_assembly_plan( + # "combinatorial_rbs_assembly_plan", + # part_documents, + # self.DVK_AE_doc, + # "BsaI", + # assembly_doc, + # ) + + # composite_list = assembly_obj.run() + # assembly_doc.write("comb_assembly.xml") + + # self.assertEqual( + # len(composite_list), + # 2, + # "Combinatorial assembly failed to produce 2 composites", + # ) + + # def test_complex_combinatorial_translation( + # self, + # ): # testing combinatorial design with 3 variable promoters and RBSs + # complex_comb_doc = sbol2.Document() + # complex_comb_doc.read("tests/test_files/complex_combinatorial_abstract.xml") + + # comb_plasmid_list = translate_abstract_to_plasmids( + # complex_comb_doc, self.plasmid_collection, self.DVK_AE_doc + # ) + + # self.assertEqual( + # len(comb_plasmid_list), + # 8, + # f"There should be 8 plasmids in the abstract translation, found {len(comb_plasmid_list)}", + # ) + + # # Run through sbol2build to test composite count + # part_documents = [] + + # for mocloPlasmid in comb_plasmid_list: + # temp_doc = sbol2.Document() + # mocloPlasmid.definition.copy(temp_doc) + # copy_sequences(mocloPlasmid.definition, temp_doc, self.plasmid_collection) + # part_documents.append(temp_doc) + + # assembly_doc = sbol2.Document() + # assembly_obj = golden_gate_assembly_plan( + # "complex_combinatorial_assembly_plan", + # part_documents, + # self.DVK_AE_doc, + # "BsaI", + # assembly_doc, + # ) + + # composite_list = assembly_obj.run() + # assembly_doc.write("complex_comb_assembly.xml") + + # self.assertEqual( + # len(composite_list), + # 9, + # f"Combinatorial assembly failed to produce 9 composites, found {len(composite_list)}", + # ) + + +if __name__ == "__main__": + unittest.main() From 7d346cfaaceba87b820dea24b3fa0811ef03c73f Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 12 May 2026 15:31:51 -0600 Subject: [PATCH 73/93] level 1 and first comb test complete --- tests/test_buildcompiler.py | 102 ++++++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 39 deletions(-) diff --git a/tests/test_buildcompiler.py b/tests/test_buildcompiler.py index aa55f5d..8bc43d4 100644 --- a/tests/test_buildcompiler.py +++ b/tests/test_buildcompiler.py @@ -13,7 +13,7 @@ from buildcompiler.buildcompiler import BuildCompiler -from buildcompiler.abstract_translator import extract_toplevel_definition +from buildcompiler.abstract_translator import extract_toplevel_definition, get_or_pull class Test_Abstract_Translation_Functions(unittest.TestCase): @@ -36,8 +36,12 @@ def setUpClass(cls): "https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1", ] + source = sbol2.Document() + + source.read("tests/test_files/combinatorial_1.xml") + cls.buildcompiler = BuildCompiler( - collections, "https://synbiohub.org", auth, sbol2.Document() + collections, "https://synbiohub.org", auth, source ) def test_simple_lvl1_assembly(self): @@ -78,18 +82,32 @@ def test_simple_lvl1_assembly(self): } for expected_uri in expected_usage_uris: - usage = product_doc.get(expected_uri) - impl = product_doc.get(usage.entity) - self.assertIn( expected_uri, usage_uris, f"Expected usage {expected_uri} was not found in assembly activity usages", ) + usage = next( + u for u in assembly_activity.usages if str(u.identity) == expected_uri + ) + + impl = get_or_pull(product_doc, self.buildcompiler.sbh, usage.entity) + self.assertIsNotNone( impl, - f"Entity {impl} should exist in the activity", + f"Entity {usage.entity} should exist in the document or on SynBioHub", + ) + + self.assertIsInstance( + impl, + sbol2.Implementation, + f"Entity {usage.entity} should be an SBOL Implementation", + ) + + self.assertIsNotNone( + impl.built, + f"Implementation {impl.identity} should reference a built object", ) expected_product_uri = "https://SBOL2Build.org/qlSBuNBL_composite_1_impl/1" @@ -104,56 +122,62 @@ def test_simple_lvl1_assembly(self): self.assertEqual( product_impl.wasGeneratedBy, - assembly_activity.identity, + [assembly_activity.identity], f"Implementation {product_impl} must include wasGeneratedBy {assembly_activity.identity}", ) self.assertEqual( - product_def, + product_def.identity, "https://SBOL2Build.org/qlSBuNBL_composite_1/1", f"Implementation {product_impl} must build {product_def}", ) product_doc.write("test_simple_lvl1_assembly.xml") - # def test_two_rbs_combinatorial_translation(self): - # comb_doc = sbol2.Document() - # comb_doc.read("tests/test_files/combinatorial_1.xml") + def test_two_rbs_combinatorial_translation(self): + comb_doc = sbol2.Document() + comb_doc.read("tests/test_files/combinatorial_1.xml") - # design = extract_toplevel_definition(comb_doc) + design = comb_doc.combinatorialderivations[0] - # self.assertEqual( - # len(comb_plasmid_list), - # 5, - # "There should be 5 plasmids in the abstract translation", - # ) + result_dict, assembly_doc = self.buildcompiler.assembly_lvl1(design) - # # Run through sbol2build to test composite count - # part_documents = [] + assembly_doc.write("comb_assembly.xml") - # for mocloPlasmid in comb_plasmid_list: - # temp_doc = sbol2.Document() - # mocloPlasmid.definition.copy(temp_doc) - # copy_sequences(mocloPlasmid.definition, temp_doc, self.plasmid_collection) - # part_documents.append(temp_doc) + self.assertEqual( + len(result_dict), + 1, + "Expected one combinatorial derivation key in result dictionary", + ) - # assembly_doc = sbol2.Document() - # assembly_obj = golden_gate_assembly_plan( - # "combinatorial_rbs_assembly_plan", - # part_documents, - # self.DVK_AE_doc, - # "BsaI", - # assembly_doc, - # ) + derivation_uri = "https://sbolcanvas.org/abstract_combinatorial/1" - # composite_list = assembly_obj.run() - # assembly_doc.write("comb_assembly.xml") + self.assertIn( + derivation_uri, + result_dict, + "Expected combinatorial derivation URI missing from results", + ) - # self.assertEqual( - # len(composite_list), - # 2, - # "Combinatorial assembly failed to produce 2 composites", - # ) + composites = result_dict[derivation_uri] + + self.assertEqual( + len(composites), + 2, + "Combinatorial assembly failed to produce 2 composites", + ) + + # ensure cobinatorial feature is satsified + for composite in composites: + components = composite.plasmid_definition.getInSequentialOrder() + + if len(components) > 3: + if components[3].displayId == "B0033": + has_b0033 = True + elif components[3].displayId == "B0032": + has_b0032 = True + + self.assertTrue(has_b0033, "No composite has B0033") + self.assertTrue(has_b0032, "No composite has B0032") # def test_complex_combinatorial_translation( # self, From da386689fe715c3f71bf2905a3fcd9b2645c1ea2 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 12 May 2026 17:27:11 -0600 Subject: [PATCH 74/93] removed superfluous prints --- src/buildcompiler/abstract_translator.py | 6 ------ src/sbol2build/sbol2build.py | 3 +-- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/buildcompiler/abstract_translator.py b/src/buildcompiler/abstract_translator.py index d82f7f4..3a0d316 100644 --- a/src/buildcompiler/abstract_translator.py +++ b/src/buildcompiler/abstract_translator.py @@ -321,9 +321,6 @@ def get_compatible_plasmids( and plasmid.fusion_sites[0] == match_to.fusion_sites[match_idx] and plasmid.fusion_sites[1] == backbone.fusion_sites[1] ): - print( - f"matched final component {plasmid.name} with {match_to.name} and {backbone.name} on fusion sites ({plasmid.fusion_sites[0]}, {plasmid.fusion_sites[1]})!" - ) selected_plasmids.append(plasmid) found = True break @@ -331,9 +328,6 @@ def get_compatible_plasmids( i < len(plasmid_dict) - 1 and plasmid.fusion_sites[0] == match_to.fusion_sites[match_idx] ): - print( - f"matched {plasmid.name} with {match_to.name} on fusion site {plasmid.fusion_sites[0]}!" - ) selected_plasmids.append(plasmid) found = True match_to = plasmid diff --git a/src/sbol2build/sbol2build.py b/src/sbol2build/sbol2build.py index db6a755..169804e 100644 --- a/src/sbol2build/sbol2build.py +++ b/src/sbol2build/sbol2build.py @@ -6,7 +6,7 @@ from typing import List, Union, Tuple from .constants import DNA_TYPES -sbol2.Config.setHomespace("https://SBOL2Build.org") +sbol2.Config.setHomespace("http://buildcompiler.org") sbol2.Config.setOption(sbol2.ConfigOptions.SBOL_COMPLIANT_URIS, True) sbol2.Config.setOption(sbol2.ConfigOptions.SBOL_TYPED_URIS, False) @@ -1010,7 +1010,6 @@ def append_extracts_to_doc( """ for extract, sequence in extract_tuples: try: - print("adding: " + extract.displayId) add_object_to_doc(extract, doc) add_object_to_doc(sequence, doc) except Exception as e: From d2fe409a24c9005b1fe6a8183ad002e5c76f16f3 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Tue, 12 May 2026 17:29:10 -0600 Subject: [PATCH 75/93] new test suite --- tests/test_abstract.py | 146 ----------- tests/test_buildcompiler.py | 159 +++++++----- tests/test_files/moclo_parts_circuit.xml | 303 +++++++++++------------ tests/test_helpers.py | 107 +++++--- 4 files changed, 319 insertions(+), 396 deletions(-) delete mode 100644 tests/test_abstract.py diff --git a/tests/test_abstract.py b/tests/test_abstract.py deleted file mode 100644 index 00ba486..0000000 --- a/tests/test_abstract.py +++ /dev/null @@ -1,146 +0,0 @@ -# test 1: test same abstract design with each possible circuit selection, ensure the promoter and terminator shift accordingly - -# test 2: inaccessible part in abstract design -> should throw informative error message - -# test 3: (FUTURE) abstract design with multiple TUs - -import sbol2 -import unittest -import sys -import os - -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) - -from sbol2build.abstract_translator import ( - translate_abstract_to_plasmids, - copy_sequences, -) - -from sbol2build import ( - golden_gate_assembly_plan, -) - - -class Test_Abstract_Translation_Functions(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.sbh = sbol2.PartShop("https://synbiohub.org") - - cls.plasmid_collection = sbol2.Document() - cls.sbh.pull( - "https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/CIDARMoCloPlasmidsKit_collection/1/b7fdc21c6601a61d3166073a9e50f2c3843e1df5/share", - cls.plasmid_collection, - ) - - cls.DVK_AE_doc = sbol2.Document() - cls.sbh.pull( - "https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_AE/1/647c5b2458567dcce6b0a37178d352b8ffa9a7fe/share", - cls.DVK_AE_doc, - ) - - def test_simple_abstract_translation(self): - abstract_design_doc = sbol2.Document() - abstract_design_doc.read("tests/test_files/moclo_parts_circuit.xml") - - mocloplasmid_list = translate_abstract_to_plasmids( - abstract_design_doc, self.plasmid_collection, self.DVK_AE_doc - ) - - self.assertEqual( - len(mocloplasmid_list), - 4, - "There should be 4 plasmids in the abstract translation", - ) - - prev_site = "A" - for plas in mocloplasmid_list: - self.assertEqual(prev_site, plas.fusion_sites[0], mocloplasmid_list) - prev_site = plas.fusion_sites[1] - - def test_two_rbs_combinatorial_translation(self): - comb_doc = sbol2.Document() - comb_doc.read("tests/test_files/combinatorial_1.xml") - - comb_plasmid_list = translate_abstract_to_plasmids( - comb_doc, self.plasmid_collection, self.DVK_AE_doc - ) - - self.assertEqual( - len(comb_plasmid_list), - 5, - "There should be 5 plasmids in the abstract translation", - ) - - # Run through sbol2build to test composite count - part_documents = [] - - for mocloPlasmid in comb_plasmid_list: - temp_doc = sbol2.Document() - mocloPlasmid.definition.copy(temp_doc) - copy_sequences(mocloPlasmid.definition, temp_doc, self.plasmid_collection) - part_documents.append(temp_doc) - - assembly_doc = sbol2.Document() - assembly_obj = golden_gate_assembly_plan( - "combinatorial_rbs_assembly_plan", - part_documents, - self.DVK_AE_doc, - "BsaI", - assembly_doc, - ) - - composite_list = assembly_obj.run() - assembly_doc.write("comb_assembly.xml") - - self.assertEqual( - len(composite_list), - 2, - "Combinatorial assembly failed to produce 2 composites", - ) - - def test_complex_combinatorial_translation( - self, - ): # testing combinatorial design with 3 variable promoters and RBSs - complex_comb_doc = sbol2.Document() - complex_comb_doc.read("tests/test_files/complex_combinatorial_abstract.xml") - - comb_plasmid_list = translate_abstract_to_plasmids( - complex_comb_doc, self.plasmid_collection, self.DVK_AE_doc - ) - - self.assertEqual( - len(comb_plasmid_list), - 8, - f"There should be 8 plasmids in the abstract translation, found {len(comb_plasmid_list)}", - ) - - # Run through sbol2build to test composite count - part_documents = [] - - for mocloPlasmid in comb_plasmid_list: - temp_doc = sbol2.Document() - mocloPlasmid.definition.copy(temp_doc) - copy_sequences(mocloPlasmid.definition, temp_doc, self.plasmid_collection) - part_documents.append(temp_doc) - - assembly_doc = sbol2.Document() - assembly_obj = golden_gate_assembly_plan( - "complex_combinatorial_assembly_plan", - part_documents, - self.DVK_AE_doc, - "BsaI", - assembly_doc, - ) - - composite_list = assembly_obj.run() - assembly_doc.write("complex_comb_assembly.xml") - - self.assertEqual( - len(composite_list), - 9, - f"Combinatorial assembly failed to produce 9 composites, found {len(composite_list)}", - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_buildcompiler.py b/tests/test_buildcompiler.py index 8bc43d4..dba20f7 100644 --- a/tests/test_buildcompiler.py +++ b/tests/test_buildcompiler.py @@ -1,13 +1,8 @@ -# test 1: test same abstract design with each possible circuit selection, ensure the promoter and terminator shift accordingly - -# test 2: inaccessible part in abstract design -> should throw informative error message - -# test 3: (FUTURE) abstract design with multiple TUs - import sbol2 import unittest import sys import os +from collections import Counter sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) @@ -16,7 +11,7 @@ from buildcompiler.abstract_translator import extract_toplevel_definition, get_or_pull -class Test_Abstract_Translation_Functions(unittest.TestCase): +class Test_Buildcompiler_Functions(unittest.TestCase): @classmethod def setUpClass(cls): username = os.environ.get("SBH_USERNAME") @@ -38,7 +33,9 @@ def setUpClass(cls): source = sbol2.Document() - source.read("tests/test_files/combinatorial_1.xml") + # preload combinatorial designs into buildcompiler context + source.read("tests/test_files/complex_combinatorial_abstract.xml") + source.append("tests/test_files/combinatorial_1.xml", True) cls.buildcompiler = BuildCompiler( collections, "https://synbiohub.org", auth, source @@ -60,7 +57,7 @@ def test_simple_lvl1_assembly(self): ) assembly_activity = product_doc.get( - "https://SBOL2Build.org/qlSBuNBL_composite_assembly/1" + "http://buildcompiler.org/qlSBuNBL_composite_assembly/1" ) self.assertEqual( @@ -72,13 +69,13 @@ def test_simple_lvl1_assembly(self): usage_uris = {u.identity for u in assembly_activity.usages} expected_usage_uris = { - "https://SBOL2Build.org/qlSBuNBL_composite_assembly/pJ23100_AB_impl/1", - "https://SBOL2Build.org/qlSBuNBL_composite_assembly/pB0034_BC_impl/1", - "https://SBOL2Build.org/qlSBuNBL_composite_assembly/pE0030_CD_impl/1", - "https://SBOL2Build.org/qlSBuNBL_composite_assembly/pB0015_DE_impl/1", - "https://SBOL2Build.org/qlSBuNBL_composite_assembly/DVK_AE_impl/1", - "https://SBOL2Build.org/qlSBuNBL_composite_assembly/BsaI_enzyme/1", - "https://SBOL2Build.org/qlSBuNBL_composite_assembly/T4_Ligase/1", + "http://buildcompiler.org/qlSBuNBL_composite_assembly/pJ23100_AB_impl/1", + "http://buildcompiler.org/qlSBuNBL_composite_assembly/pB0034_BC_impl/1", + "http://buildcompiler.org/qlSBuNBL_composite_assembly/pE0030_CD_impl/1", + "http://buildcompiler.org/qlSBuNBL_composite_assembly/pB0015_DE_impl/1", + "http://buildcompiler.org/qlSBuNBL_composite_assembly/DVK_AE_impl/1", + "http://buildcompiler.org/qlSBuNBL_composite_assembly/BsaI_enzyme/1", + "http://buildcompiler.org/qlSBuNBL_composite_assembly/T4_Ligase/1", } for expected_uri in expected_usage_uris: @@ -110,7 +107,7 @@ def test_simple_lvl1_assembly(self): f"Implementation {impl.identity} should reference a built object", ) - expected_product_uri = "https://SBOL2Build.org/qlSBuNBL_composite_1_impl/1" + expected_product_uri = "http://buildcompiler.org/qlSBuNBL_composite_1_impl/1" product_impl = product_doc.get(expected_product_uri) product_def = product_doc.get(product_impl.built) @@ -128,12 +125,10 @@ def test_simple_lvl1_assembly(self): self.assertEqual( product_def.identity, - "https://SBOL2Build.org/qlSBuNBL_composite_1/1", + "http://buildcompiler.org/qlSBuNBL_composite_1/1", f"Implementation {product_impl} must build {product_def}", ) - product_doc.write("test_simple_lvl1_assembly.xml") - def test_two_rbs_combinatorial_translation(self): comb_doc = sbol2.Document() comb_doc.read("tests/test_files/combinatorial_1.xml") @@ -142,8 +137,6 @@ def test_two_rbs_combinatorial_translation(self): result_dict, assembly_doc = self.buildcompiler.assembly_lvl1(design) - assembly_doc.write("comb_assembly.xml") - self.assertEqual( len(result_dict), 1, @@ -179,48 +172,86 @@ def test_two_rbs_combinatorial_translation(self): self.assertTrue(has_b0033, "No composite has B0033") self.assertTrue(has_b0032, "No composite has B0032") - # def test_complex_combinatorial_translation( - # self, - # ): # testing combinatorial design with 3 variable promoters and RBSs - # complex_comb_doc = sbol2.Document() - # complex_comb_doc.read("tests/test_files/complex_combinatorial_abstract.xml") - - # comb_plasmid_list = translate_abstract_to_plasmids( - # complex_comb_doc, self.plasmid_collection, self.DVK_AE_doc - # ) - - # self.assertEqual( - # len(comb_plasmid_list), - # 8, - # f"There should be 8 plasmids in the abstract translation, found {len(comb_plasmid_list)}", - # ) - - # # Run through sbol2build to test composite count - # part_documents = [] - - # for mocloPlasmid in comb_plasmid_list: - # temp_doc = sbol2.Document() - # mocloPlasmid.definition.copy(temp_doc) - # copy_sequences(mocloPlasmid.definition, temp_doc, self.plasmid_collection) - # part_documents.append(temp_doc) - - # assembly_doc = sbol2.Document() - # assembly_obj = golden_gate_assembly_plan( - # "complex_combinatorial_assembly_plan", - # part_documents, - # self.DVK_AE_doc, - # "BsaI", - # assembly_doc, - # ) - - # composite_list = assembly_obj.run() - # assembly_doc.write("complex_comb_assembly.xml") - - # self.assertEqual( - # len(composite_list), - # 9, - # f"Combinatorial assembly failed to produce 9 composites, found {len(composite_list)}", - # ) + def test_complex_combinatorial_translation( + self, + ): # testing combinatorial design with 3 variable promoters and RBSs + complex_comb_doc = sbol2.Document() + complex_comb_doc.read("tests/test_files/complex_combinatorial_abstract.xml") + + design = complex_comb_doc.combinatorialderivations[0] + + result_dict, assembly_doc = self.buildcompiler.assembly_lvl1(design) + + assembly_doc.write("comb_assembly.xml") + + self.assertEqual( + len(result_dict), + 1, + "Expected one combinatorial derivation key in result dictionary", + ) + + derivation_uri = "https://sbolcanvas.org/dEOuAjnj/1" + + self.assertIn( + derivation_uri, + result_dict, + "Expected combinatorial derivation URI missing from results", + ) + + composites = result_dict[derivation_uri] + + self.assertEqual( + len(composites), + 9, + f"Combinatorial assembly failed to produce 9 composites, found {len(composites)}", + ) + + promoter_counts = Counter() + rbs_counts = Counter() + + for composite in composites: + components = composite.plasmid_definition.getInSequentialOrder() + display_ids = [component.displayId for component in components] + print(display_ids) + + promoter_counts[components[1].displayId] += 1 # index 1 = promoter + rbs_counts[components[3].displayId] += 1 # index 3 = RBS + + self.assertEqual( + promoter_counts["J23100"], + 3, + f"Expected J23100 to appear 3 times across the composite dictionary, found {promoter_counts['J23100']}", + ) + + self.assertEqual( + promoter_counts["J23106"], + 3, + f"Expected J23106 to appear 3 times across the composite dictionary, found {promoter_counts['J23106']}", + ) + + self.assertEqual( + promoter_counts["J23116"], + 3, + f"Expected J23116 to appear 3 times across the composite dictionary, found {promoter_counts['J23116']}", + ) + + self.assertEqual( + rbs_counts["B0034"], + 3, + f"Expected B0034 to appear 3 times across the composite dictionary, found {rbs_counts['B0034']}", + ) + + self.assertEqual( + rbs_counts["B0032"], + 3, + f"Expected B0032 to appear 3 times across the composite dictionary, found {rbs_counts['B0032']}", + ) + + self.assertEqual( + rbs_counts["B0033"], + 3, + f"Expected B0033 to appear 3 times across the composite dictionary, found {rbs_counts['B0033']}", + ) if __name__ == "__main__": diff --git a/tests/test_files/moclo_parts_circuit.xml b/tests/test_files/moclo_parts_circuit.xml index a71bac7..2c47e00 100644 --- a/tests/test_files/moclo_parts_circuit.xml +++ b/tests/test_files/moclo_parts_circuit.xml @@ -1,37 +1,35 @@ - - - - - moclo_circuit - - - - i6TQvNp91_28 - - - - - - - - - i6TQvNp91 + + + + + E0030_yfp + 1 + E0030_yfp + MoClo Basic Part: CDS - Fluorescent protein. Yellow. + 26479688 + + + + + + + qlSBuNBL 1 - - - E0030_yfp_3 + + + J23100_1 1 - + - - + + B0034_2 1 @@ -39,117 +37,137 @@ - - - J23100_1 + + + B0015_4 1 - + - - - B0015_4 + + + E0030_yfp_3 1 - + - - - i6TQvNp91Annotation2 + + + qlSBuNBLAnnotation3 1 - - - location2 + + + location3 1 + 780 + 908 - + - + - - - i6TQvNp91Annotation1 + + + qlSBuNBLAnnotation0 1 - - - location1 + + + location0 1 + 1 + 35 - + - + - - - i6TQvNp91Annotation0 + + + qlSBuNBLAnnotation2 1 - - - location0 + + + location2 1 + 57 + 779 - + - + - - - i6TQvNp91Annotation3 + + + qlSBuNBLAnnotation1 1 - - - location3 + + + location1 1 + 36 + 56 - + - + - - - i6TQvNp91Constraint2 + + + qlSBuNBLConstraint2 1 - - + + - - - i6TQvNp91Constraint3 + + + qlSBuNBLConstraint1 1 - - + + - - - i6TQvNp91Constraint1 + + + qlSBuNBLConstraint3 1 - - + + + + + + + B0034 + 1 + B0034 + MoClo Basic Part: RBS - Weiss RBS, high strength. Modified from Bba_B0034 to adjust spacing in MC system. + 26479688 + + + @@ -157,27 +175,10 @@ 1 B0015 MoClo Basic Part: Double terminator (B0010:B0012) - - - - 26479688 - - - - E0030_yfp - 1 - E0030_yfp - MoClo Basic Part: CDS - Fluorescent protein. Yellow. - - - - - 26479688 - - + @@ -185,68 +186,61 @@ 1 J23100 MoClo Basic Part: Constitutive promoter - Anderson series - high strength - - - - 26479688 + - - - B0034 - 1 - B0034 - MoClo Basic Part: RBS - Weiss RBS, high strength. Modified from Bba_B0034 to adjust spacing in MC system. - - - - - 26479688 - - - + + + J23100_sequence + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGC + + + + + E0030_yfp_sequence + ATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCTTCGGCTACGGCCTGCAATGCTTCGCCCGCTACCCCGACCACATGAAGCTGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCTACCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAATAA + + + + + B0015_sequence + CCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + + + B0034_sequence + AGAGAAAGAGGAGAAATACTA + + + + + qlSBuNBL_sequence + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCAGAGAAAGAGGAGAAATACTA + + J23100_Layout - - - moclo_circuit_Layout - + + + qlSBuNBL_Layout + - - 720.0 - 339.0 - 200.0 - 100.0 - i6TQvNp91_28 - - - - - - - B0015_Layout - - - - - i6TQvNp91_Layout - - - - 720.0 - 339.0 + + 412.5 + 337.0 200.0 100.0 container - + 0.0 50.0 200.0 @@ -255,7 +249,7 @@ - + 0.0 0.0 50.0 @@ -263,9 +257,9 @@ J23100_1 - + - + 50.0 0.0 50.0 @@ -273,9 +267,9 @@ B0034_2 - + - + 100.0 0.0 50.0 @@ -283,9 +277,9 @@ E0030_yfp_3 - + - + 150.0 0.0 50.0 @@ -293,7 +287,12 @@ B0015_4 - + + + + + B0015_Layout + @@ -305,4 +304,4 @@ B0034_Layout - + \ No newline at end of file diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 5600e7f..6154f85 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,43 +1,81 @@ import sbol2 -import filecmp import os import sys -import tempfile import unittest -from pathlib import Path -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src'))) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) + +from buildcompiler.sbol2build import ( + rebase_restriction_enzyme, + dna_componentdefinition_with_sequence, + number_to_suffix, + is_circular, + append_extracts_to_doc, +) -from sbol2build import rebase_restriction_enzyme, dna_componentdefinition_with_sequence, number_to_suffix, is_circular, append_extracts_to_doc, part_in_backbone_from_sbol class Test_HelperFunctions(unittest.TestCase): def test_restriction_enzyme(self): bsai = rebase_restriction_enzyme(name="BsaI") - constructor_error = 'Constructor Error: ed_restriction_enzyme' + constructor_error = "Constructor Error: ed_restriction_enzyme" test_cases = [ - ('http://rebase.neb.com/rebase/enz/BsaI.html', bsai.wasDerivedFrom, constructor_error), - ('http://www.biopax.org/release/biopax-level3.owl#Protein', bsai.types, constructor_error), - ('BsaI', bsai.name, constructor_error), - ('Restriction enzyme BsaI from REBASE.', bsai.description, constructor_error) + ( + "http://rebase.neb.com/rebase/enz/BsaI.html", + bsai.wasDerivedFrom, + constructor_error, + ), + ( + "http://www.biopax.org/release/biopax-level3.owl#Protein", + bsai.types, + constructor_error, + ), + ("BsaI", bsai.name, constructor_error), + ( + "Restriction enzyme BsaI from REBASE.", + bsai.description, + constructor_error, + ), ] for expected, attribute, error_msg in test_cases: with self.subTest(expected=expected, attribute=attribute): self.assertIn(expected, attribute, error_msg) - def test_dna_component_and_sequence(self): + def test_dna_component_and_sequence(self): # create a test dna component - dna_identity = 'Test_dna_identity' - dna_sequence = 'Test_dna_sequence' - test_dna_component, test_sequence = dna_componentdefinition_with_sequence(dna_identity, dna_sequence) + dna_identity = "Test_dna_identity" + dna_sequence = "Test_dna_sequence" + test_dna_component, test_sequence = dna_componentdefinition_with_sequence( + dna_identity, dna_sequence + ) test_cases = [ - ("", repr(type(test_dna_component)), 'Constructor Error: dna_componentdefinition_with_sequence, Not a ComponentDefinition'), - ("", repr(type(test_sequence)), 'Constructor Error: dna_componentdefinition_with_sequence, Not a Sequence'), - (f"https://SBOL2Build.org/{dna_identity}_seq/1", test_sequence.identity, 'Constructor Error: dna_componentdefinition_with_sequence, Incorrect Identity'), - ([test_sequence.identity], test_dna_component.sequences, 'Constructor Error: dna_componentdefinition_with_sequence, Sequence not in ComponentDefinition Sequences List'), - (['http://www.biopax.org/release/biopax-level3.owl#DnaRegion'], test_dna_component.types, 'Constructor Error: dna_componentdefinition_with_sequence, Missing DNA type') + ( + "", + repr(type(test_dna_component)), + "Constructor Error: dna_componentdefinition_with_sequence, Not a ComponentDefinition", + ), + ( + "", + repr(type(test_sequence)), + "Constructor Error: dna_componentdefinition_with_sequence, Not a Sequence", + ), + ( + f"http://buildcompiler.org/{dna_identity}_seq/1", + test_sequence.identity, + "Constructor Error: dna_componentdefinition_with_sequence, Incorrect Identity", + ), + ( + [test_sequence.identity], + test_dna_component.sequences, + "Constructor Error: dna_componentdefinition_with_sequence, Sequence not in ComponentDefinition Sequences List", + ), + ( + ["http://www.biopax.org/release/biopax-level3.owl#DnaRegion"], + test_dna_component.types, + "Constructor Error: dna_componentdefinition_with_sequence, Missing DNA type", + ), ] for expected, attribute, error_msg in test_cases: @@ -46,7 +84,7 @@ def test_dna_component_and_sequence(self): def test_is_circular(self): comp_def_circ = sbol2.ComponentDefinition("test") - comp_def_circ.types = ['http://identifiers.org/so/SO:0000988'] + comp_def_circ.types = ["http://identifiers.org/so/SO:0000988"] comp_def_not_circ = sbol2.ComponentDefinition("test_not_circ") @@ -54,38 +92,39 @@ def test_is_circular(self): self.assertFalse(is_circular(comp_def_not_circ)) def test_number_to_suffix(self): - #1 letter + # 1 letter self.assertEqual(number_to_suffix(1), "A") self.assertEqual(number_to_suffix(2), "B") self.assertEqual(number_to_suffix(26), "Z") - #2 letters + # 2 letters self.assertEqual(number_to_suffix(27), "AA") self.assertEqual(number_to_suffix(28), "AB") self.assertEqual(number_to_suffix(52), "AZ") self.assertEqual(number_to_suffix(53), "BA") - #3 letters - self.assertEqual(number_to_suffix(702), "ZZ") # 26*27 - 1 + # 3 letters + self.assertEqual(number_to_suffix(702), "ZZ") # 26*27 - 1 self.assertEqual(number_to_suffix(703), "AAA") # 26*27 self.assertEqual(number_to_suffix(704), "AAB") # 26*27 + 1 self.assertEqual(number_to_suffix(0), "") def test_append_extracts_to_doc(self): doc = sbol2.Document() - tup1 = dna_componentdefinition_with_sequence('def1', 'atgcaatg') - tup2 = dna_componentdefinition_with_sequence('def2', 'ggacttaac') + tup1 = dna_componentdefinition_with_sequence("def1", "atgcaatg") + tup2 = dna_componentdefinition_with_sequence("def2", "ggacttaac") append_extracts_to_doc([tup1, tup2, tup1], doc) - #ensure duplicate of tup1 is not being counted + # ensure duplicate of tup1 is not being counted self.assertEqual(len(doc.sequences), 2) self.assertEqual(len(doc.componentDefinitions), 2) - self.assertEqual(doc.sequences[0].elements, 'atgcaatg') - self.assertEqual(doc.componentDefinitions[0].displayId, 'def1') - self.assertEqual(doc.sequences[1].elements, 'ggacttaac') - self.assertEqual(doc.componentDefinitions[1].displayId, 'def2') + self.assertEqual(doc.sequences[0].elements, "atgcaatg") + self.assertEqual(doc.componentDefinitions[0].displayId, "def1") + self.assertEqual(doc.sequences[1].elements, "ggacttaac") + self.assertEqual(doc.componentDefinitions[1].displayId, "def2") + + # TODO test for part in backbone? - #TODO test for part in backbone? -if __name__ == '__main__': - unittest.main() \ No newline at end of file +if __name__ == "__main__": + unittest.main() From feb64fa6e6a00ad3966e0aaca63342258781630b Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Wed, 13 May 2026 12:34:15 -0600 Subject: [PATCH 76/93] synbiohub.org->api.synbiohub.org --- tests/test_buildcompiler.py | 8 ++++---- tests/test_core.py | 30 +++++++++++++++--------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/test_buildcompiler.py b/tests/test_buildcompiler.py index dba20f7..0313db7 100644 --- a/tests/test_buildcompiler.py +++ b/tests/test_buildcompiler.py @@ -21,14 +21,14 @@ def setUpClass(cls): raise RuntimeError( "Missing SBH_USERNAME and/or SBH_PASSWORD environment variables" ) - sbh = sbol2.PartShop("https://synbiohub.org") + sbh = sbol2.PartShop("https://api.synbiohub.org") sbh.login(username, password) auth = sbh.key collections = [ - "https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1", - "https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1", + "https://api.synbiohub.org/user/Gon/impl_test/impl_test_collection/1", + "https://api.synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1", ] source = sbol2.Document() @@ -38,7 +38,7 @@ def setUpClass(cls): source.append("tests/test_files/combinatorial_1.xml", True) cls.buildcompiler = BuildCompiler( - collections, "https://synbiohub.org", auth, source + collections, "https://api.synbiohub.org", auth, source ) def test_simple_lvl1_assembly(self): diff --git a/tests/test_core.py b/tests/test_core.py index cc5c51b..6d6080a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -27,7 +27,7 @@ class Test_Assembly_Functions(unittest.TestCase): @classmethod def setUpClass(cls): - cls.sbh = sbol2.PartShop("https://synbiohub.org") + cls.sbh = sbol2.PartShop("https://api.synbiohub.org") username = os.environ.get("SBH_USERNAME") password = os.environ.get("SBH_PASSWORD") @@ -43,27 +43,27 @@ def setUpClass(cls): final_doc = sbol2.Document() cls.sbh.pull( - "https://synbiohub.org/user/Gon/CIDARMoCloParts/CIDARMoCloParts_collection/1", + "https://api.synbiohub.org/user/Gon/CIDARMoCloParts/CIDARMoCloParts_collection/1", cls.source_doc, ) cls.sbh.pull( - "https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/CIDARMoCloPlasmidsKit_collection/1", + "https://api.synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/CIDARMoCloPlasmidsKit_collection/1", cls.source_doc, ) cls.sbh.pull( - "https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1", + "https://api.synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1", cls.source_doc, ) cls.sbh.pull( - "https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1", + "https://api.synbiohub.org/user/Gon/impl_test/impl_test_collection/1", cls.source_doc, ) cls.re_impl = cls.source_doc.get( - "https://synbiohub.org/user/Gon/Enzyme_Implementations/BsaI_impl/1" + "https://api.synbiohub.org/user/Gon/Enzyme_Implementations/BsaI_impl/1" ) cls.ligase_impl = cls.source_doc.get( - "https://synbiohub.org/user/Gon/Enzyme_Implementations/T4_Ligase_impl/1" + "https://api.synbiohub.org/user/Gon/Enzyme_Implementations/T4_Ligase_impl/1" ) cls.assembly = Assembly( @@ -72,7 +72,7 @@ def setUpClass(cls): def test_part_digestion(self): # TODO test activity relationships impl = self.source_doc.get( - "https://synbiohub.org/user/Gon/impl_test/pJ23100_AB_impl/1" + "https://api.synbiohub.org/user/Gon/impl_test/pJ23100_AB_impl/1" ) definition = self.source_doc.get(impl.built) plasmid = Plasmid(definition, None, [impl], None, self.source_doc) @@ -152,7 +152,7 @@ def test_part_digestion(self): # TODO test activity relationships def test_backbone_digestion(self): impl = self.source_doc.get( - "https://synbiohub.org/user/Gon/impl_test/DVK_AE_impl/1" + "https://api.synbiohub.org/user/Gon/impl_test/DVK_AE_impl/1" ) definition = self.source_doc.get(impl.built) plasmid = Plasmid(definition, None, [impl], None, self.source_doc) @@ -235,16 +235,16 @@ def test_ligation(self): assembly_activity = self.assembly.initialize_assembly_activity() parts = [ self.source_doc.get( - "https://synbiohub.org/user/Gon/impl_test/pJ23100_AB_impl/1" + "https://api.synbiohub.org/user/Gon/impl_test/pJ23100_AB_impl/1" ), self.source_doc.get( - "https://synbiohub.org/user/Gon/impl_test/pB0034_BC_impl/1" + "https://api.synbiohub.org/user/Gon/impl_test/pB0034_BC_impl/1" ), self.source_doc.get( - "https://synbiohub.org/user/Gon/impl_test/pE0030_CD_impl/1" + "https://api.synbiohub.org/user/Gon/impl_test/pE0030_CD_impl/1" ), self.source_doc.get( - "https://synbiohub.org/user/Gon/impl_test/pB0015_DE_impl/1" + "https://api.synbiohub.org/user/Gon/impl_test/pB0015_DE_impl/1" ), ] @@ -269,14 +269,14 @@ def test_ligation(self): reactants_list.append(extracts_tuple_list[0][0]) backbone_impl = self.source_doc.get( - "https://synbiohub.org/user/Gon/impl_test/DVK_AE_impl/1" + "https://api.synbiohub.org/user/Gon/impl_test/DVK_AE_impl/1" ) # run digestion, extract component + sequence, add to ligation_doc, reactants_list definition = self.source_doc.get(backbone_impl.built) self.sbh.pull( - "https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_AE/1", + "https://api.synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_AE/1", self.source_doc, ) From 3384148e5266751d471e3fd6af38e63d06133cb9 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Wed, 13 May 2026 12:39:54 -0600 Subject: [PATCH 77/93] secret access for github job --- .github/workflows/python-package.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index e22f5fb..44ce5a5 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -37,5 +37,8 @@ jobs: # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with unittest + env: + SBH_USERNAME: ${{ secrets.SBH_USERNAME }} + SBH_PASSWORD: ${{ secrets.SBH_PASSWORD }} run: | python -m unittest discover -s tests From cecf73ff054e7658283b10b5b8d3ff5dd997311f Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Wed, 13 May 2026 12:58:06 -0600 Subject: [PATCH 78/93] api for pulls only --- tests/test_buildcompiler.py | 6 +++--- tests/test_core.py | 24 ++++++++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/test_buildcompiler.py b/tests/test_buildcompiler.py index 0313db7..2663e22 100644 --- a/tests/test_buildcompiler.py +++ b/tests/test_buildcompiler.py @@ -27,8 +27,8 @@ def setUpClass(cls): auth = sbh.key collections = [ - "https://api.synbiohub.org/user/Gon/impl_test/impl_test_collection/1", - "https://api.synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1", + "https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1", + "https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1", ] source = sbol2.Document() @@ -38,7 +38,7 @@ def setUpClass(cls): source.append("tests/test_files/combinatorial_1.xml", True) cls.buildcompiler = BuildCompiler( - collections, "https://api.synbiohub.org", auth, source + collections, "https://synbiohub.org", auth, source ) def test_simple_lvl1_assembly(self): diff --git a/tests/test_core.py b/tests/test_core.py index 6d6080a..868bd7a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -60,10 +60,10 @@ def setUpClass(cls): ) cls.re_impl = cls.source_doc.get( - "https://api.synbiohub.org/user/Gon/Enzyme_Implementations/BsaI_impl/1" + "https://synbiohub.org/user/Gon/Enzyme_Implementations/BsaI_impl/1" ) cls.ligase_impl = cls.source_doc.get( - "https://api.synbiohub.org/user/Gon/Enzyme_Implementations/T4_Ligase_impl/1" + "https://synbiohub.org/user/Gon/Enzyme_Implementations/T4_Ligase_impl/1" ) cls.assembly = Assembly( @@ -72,7 +72,7 @@ def setUpClass(cls): def test_part_digestion(self): # TODO test activity relationships impl = self.source_doc.get( - "https://api.synbiohub.org/user/Gon/impl_test/pJ23100_AB_impl/1" + "https://synbiohub.org/user/Gon/impl_test/pJ23100_AB_impl/1" ) definition = self.source_doc.get(impl.built) plasmid = Plasmid(definition, None, [impl], None, self.source_doc) @@ -152,7 +152,7 @@ def test_part_digestion(self): # TODO test activity relationships def test_backbone_digestion(self): impl = self.source_doc.get( - "https://api.synbiohub.org/user/Gon/impl_test/DVK_AE_impl/1" + "https://synbiohub.org/user/Gon/impl_test/DVK_AE_impl/1" ) definition = self.source_doc.get(impl.built) plasmid = Plasmid(definition, None, [impl], None, self.source_doc) @@ -235,16 +235,16 @@ def test_ligation(self): assembly_activity = self.assembly.initialize_assembly_activity() parts = [ self.source_doc.get( - "https://api.synbiohub.org/user/Gon/impl_test/pJ23100_AB_impl/1" + "https://synbiohub.org/user/Gon/impl_test/pJ23100_AB_impl/1" ), self.source_doc.get( - "https://api.synbiohub.org/user/Gon/impl_test/pB0034_BC_impl/1" + "https://synbiohub.org/user/Gon/impl_test/pB0034_BC_impl/1" ), self.source_doc.get( - "https://api.synbiohub.org/user/Gon/impl_test/pE0030_CD_impl/1" + "https://synbiohub.org/user/Gon/impl_test/pE0030_CD_impl/1" ), self.source_doc.get( - "https://api.synbiohub.org/user/Gon/impl_test/pB0015_DE_impl/1" + "https://synbiohub.org/user/Gon/impl_test/pB0015_DE_impl/1" ), ] @@ -269,7 +269,7 @@ def test_ligation(self): reactants_list.append(extracts_tuple_list[0][0]) backbone_impl = self.source_doc.get( - "https://api.synbiohub.org/user/Gon/impl_test/DVK_AE_impl/1" + "https://synbiohub.org/user/Gon/impl_test/DVK_AE_impl/1" ) # run digestion, extract component + sequence, add to ligation_doc, reactants_list @@ -301,7 +301,11 @@ def test_ligation(self): reactants_list.append(extracts_tuple_list[0][0]) ligation_doc.add_list([self.re_impl, self.ligase_impl]) - self.sbh.pull(self.ligase_impl.built, ligation_doc) + + pull_uri = self.ligase_impl.built.replace( + "https://synbiohub.org", "https://api.synbiohub.org" + ) + self.sbh.pull(pull_uri, ligation_doc) final_doc = sbol2.Document() From 26e638d13f3e39d4dd0c1ba6c2ca4d8381bbf4a0 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Wed, 13 May 2026 13:00:44 -0600 Subject: [PATCH 79/93] api for collections --- tests/test_buildcompiler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_buildcompiler.py b/tests/test_buildcompiler.py index 2663e22..f76e411 100644 --- a/tests/test_buildcompiler.py +++ b/tests/test_buildcompiler.py @@ -27,8 +27,8 @@ def setUpClass(cls): auth = sbh.key collections = [ - "https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1", - "https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1", + "https://api.synbiohub.org/user/Gon/impl_test/impl_test_collection/1", + "https://api.synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1", ] source = sbol2.Document() From f91e4903d258c7fafccedec1287c3d1beff95879 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Wed, 13 May 2026 13:11:27 -0600 Subject: [PATCH 80/93] revert api for collections --- tests/test_buildcompiler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_buildcompiler.py b/tests/test_buildcompiler.py index f76e411..2663e22 100644 --- a/tests/test_buildcompiler.py +++ b/tests/test_buildcompiler.py @@ -27,8 +27,8 @@ def setUpClass(cls): auth = sbh.key collections = [ - "https://api.synbiohub.org/user/Gon/impl_test/impl_test_collection/1", - "https://api.synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1", + "https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1", + "https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1", ] source = sbol2.Document() From bb8d3382f171d3189d1f8931d82cdbac05619ca2 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Wed, 13 May 2026 14:07:50 -0600 Subject: [PATCH 81/93] added server mode flag to add api. to canonical sbh registry uris --- src/buildcompiler/abstract_translator.py | 15 +++++++++++--- src/buildcompiler/buildcompiler.py | 25 ++++++++++++++++++------ tests/test_buildcompiler.py | 4 ++-- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/buildcompiler/abstract_translator.py b/src/buildcompiler/abstract_translator.py index 3a0d316..327c248 100644 --- a/src/buildcompiler/abstract_translator.py +++ b/src/buildcompiler/abstract_translator.py @@ -135,17 +135,26 @@ def copy_sequences(component_definition, target_doc, collection_doc): seq_obj.copy(target_doc) -def get_or_pull(doc, sbh, uri): +def get_or_pull( + doc: sbol2.Document, sbh: sbol2.PartShop, uri: str, server_mode: bool = False +): """ Get an SBOL object from a Document. If missing, pull it from SynBioHub and retry. """ + try: return doc.get(uri) except Exception as e: - # Treat lookup failure as "not present" - sbh.pull(uri, doc) + pull_uri = uri + + if server_mode: + canonical_resource = sbh.resource.replace("://api.", "://") + + pull_uri = uri.replace(canonical_resource, sbh.resource) + + sbh.pull(pull_uri, doc) try: return doc.get(uri) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index b31c64f..f345ba7 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -50,6 +50,7 @@ def __init__( sbh_registry: str, auth_token: str, sbol_doc: sbol2.Document = None, + server_mode: bool = False, ): self.sbh = sbol2.PartShop(sbh_registry) self.sbh.key = auth_token @@ -59,6 +60,7 @@ def __init__( self.BsaI_impl = None self.BbsI_impl = None self.T4_ligase_impl = None + self.server_mode = server_mode self._index_collections(collections) @@ -75,11 +77,16 @@ def _index_collections(self, collections: List[str]): :rtype: None """ for uri in collections: + if self.server_mode: + canonical_resource = self.sbh.resource.replace("://api.", "://") + uri = uri.replace(canonical_resource, self.sbh.resource) print(f"Indexing collection: {uri}") self.sbh.pull(uri, self.sbol_doc) for implementation in self.sbol_doc.implementations: - built_object = get_or_pull(self.sbol_doc, self.sbh, implementation.built) + built_object = get_or_pull( + self.sbol_doc, self.sbh, implementation.built, self.server_mode + ) if ( type(built_object) is sbol2.ModuleDefinition and ORGANISM_STRAIN in built_object.roles @@ -531,7 +538,9 @@ def _extract_plasmids_from_strain( ): # strain_implementation = optional param for plasmid in strain.functionalComponents: - plasmid_definition = get_or_pull(doc, self.sbh, plasmid.definition) + plasmid_definition = get_or_pull( + doc, self.sbh, plasmid.definition, self.server_mode + ) if ENGINEERED_PLASMID in plasmid_definition.roles: existing = self._get_indexed_plasmid( @@ -664,7 +673,7 @@ def _extract_design_parts( """ component_list = [c for c in design.getInSequentialOrder()] return [ - get_or_pull(self.sbol_doc, self.sbh, component.definition) + get_or_pull(self.sbol_doc, self.sbh, component.definition, self.server_mode) for component in component_list ] @@ -698,7 +707,9 @@ def extract_combinatorial_design_parts( component_list = [c for c in design.getInSequentialOrder()] component_dict = { component.identity: [ - get_or_pull(self.sbol_doc, self.sbh, component.definition) + get_or_pull( + self.sbol_doc, self.sbh, component.definition, self.server_mode + ) ] for component in component_list } @@ -720,7 +731,9 @@ def _get_abstract_design(self) -> sbol2.ComponentDefinition: continue component_definitions = [ - get_or_pull(self.sbol_doc, self.sbh, component.definition) + get_or_pull( + self.sbol_doc, self.sbh, component.definition, self.server_mode + ) for component in definition.getInSequentialOrder() ] if any( @@ -782,7 +795,7 @@ def _is_single_part(self, plasmid: sbol2.ComponentDefinition) -> bool: return False else: component_definitions = [ - get_or_pull(self.sbol_doc, self.sbh, comp.definition) + get_or_pull(self.sbol_doc, self.sbh, comp.definition, self.server_mode) for comp in plasmid.getInSequentialOrder() ] diff --git a/tests/test_buildcompiler.py b/tests/test_buildcompiler.py index 2663e22..c407c5b 100644 --- a/tests/test_buildcompiler.py +++ b/tests/test_buildcompiler.py @@ -38,7 +38,7 @@ def setUpClass(cls): source.append("tests/test_files/combinatorial_1.xml", True) cls.buildcompiler = BuildCompiler( - collections, "https://synbiohub.org", auth, source + collections, "https://api.synbiohub.org", auth, source, server_mode=True ) def test_simple_lvl1_assembly(self): @@ -89,7 +89,7 @@ def test_simple_lvl1_assembly(self): u for u in assembly_activity.usages if str(u.identity) == expected_uri ) - impl = get_or_pull(product_doc, self.buildcompiler.sbh, usage.entity) + impl = get_or_pull(product_doc, self.buildcompiler.sbh, usage.entity, True) self.assertIsNotNone( impl, From 776bc73e2ff5629025c17b048a4044dc45d436fc Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Wed, 13 May 2026 17:46:15 -0600 Subject: [PATCH 82/93] notebooks for creating various implementations used in buildcompiler --- notebooks/enzyme_creation.ipynb | 111 +++++++ notebooks/impl_creation.ipynb | 511 ++++++++++++++++++++++++++++++++ 2 files changed, 622 insertions(+) create mode 100644 notebooks/enzyme_creation.ipynb create mode 100644 notebooks/impl_creation.ipynb diff --git a/notebooks/enzyme_creation.ipynb b/notebooks/enzyme_creation.ipynb new file mode 100644 index 0000000..d26eec2 --- /dev/null +++ b/notebooks/enzyme_creation.ipynb @@ -0,0 +1,111 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "87bdb42e", + "metadata": {}, + "outputs": [], + "source": [ + "import sbol2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "00a6e6c9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Valid.'" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from buildcompiler.sbol2build import rebase_restriction_enzyme, add_object_to_doc\n", + "\n", + "sbol2.Config.setHomespace(\"https://SBOL2Build.org\")\n", + "sbol2.Config.setOption(sbol2.ConfigOptions.SBOL_COMPLIANT_URIS, True)\n", + "sbol2.Config.setOption(sbol2.ConfigOptions.SBOL_TYPED_URIS, False)\n", + "\n", + "protein_doc = sbol2.Document()\n", + "\n", + "\n", + "BsaI_def = rebase_restriction_enzyme(\"BsaI\")\n", + "\n", + "RE_sourcing = sbol2.Activity(\"restriction_enzyme_purchase\")\n", + "RE_sourcing.name = \"Restriction Enzyme Purchase\"\n", + "\n", + "BsaI_imp = sbol2.Implementation(f\"{BsaI_def.displayId}_impl\")\n", + "\n", + "BsaI_imp.built = BsaI_def.identity\n", + "BsaI_imp.wasGeneratedBy = RE_sourcing.identity\n", + "\n", + "BbsI_def = rebase_restriction_enzyme(\"BbsI\")\n", + "\n", + "BbsI_imp = sbol2.Implementation(f\"{BbsI_def.displayId}_impl\")\n", + "\n", + "BbsI_imp.built = BbsI_def.identity\n", + "BbsI_imp.wasGeneratedBy = RE_sourcing.identity\n", + "\n", + "\n", + "ligase = sbol2.ComponentDefinition(\"T4_Ligase\")\n", + "ligase.name = \"T4_Ligase\"\n", + "ligase.types = [sbol2.BIOPAX_PROTEIN]\n", + "ligase.roles = [\"http://identifiers.org/ncit/NCIT:C16796\"]\n", + "\n", + "ligase_sourcing = sbol2.Activity(\"ligase_purchase\")\n", + "ligase_sourcing.name = \"Ligase Purchase\"\n", + "\n", + "T4_imp = sbol2.Implementation(f\"{ligase.displayId}_impl\")\n", + "T4_imp.built = ligase.identity\n", + "T4_imp.wasGeneratedBy = ligase_sourcing.identity\n", + "\n", + "add_object_to_doc(ligase, protein_doc)\n", + "add_object_to_doc(T4_imp, protein_doc)\n", + "add_object_to_doc(BsaI_def, protein_doc)\n", + "add_object_to_doc(BsaI_imp, protein_doc)\n", + "add_object_to_doc(BbsI_def, protein_doc)\n", + "add_object_to_doc(BbsI_imp, protein_doc)\n", + "add_object_to_doc(RE_sourcing, protein_doc)\n", + "\n", + "protein_doc.write(\"proteins.xml\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f7433c3", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (buildplanner)", + "language": "python", + "name": "buildplanner" + }, + "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.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/impl_creation.ipynb b/notebooks/impl_creation.ipynb new file mode 100644 index 0000000..a87de5e --- /dev/null +++ b/notebooks/impl_creation.ipynb @@ -0,0 +1,511 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 6, + "id": "87bdb42e", + "metadata": {}, + "outputs": [], + "source": [ + "import sbol2\n", + "from buildcompiler.buildcompiler import BuildCompiler" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "90648527", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Indexing collection: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/CIDARMoCloPlasmidsKit_collection/1\n" + ] + } + ], + "source": [ + "auth = \"0b2dc76c-4c1d-4ee8-b339-6a3e15e3faf9\"\n", + "buildcompiler = BuildCompiler(\n", + " [\n", + " \"https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/CIDARMoCloPlasmidsKit_collection/1\"\n", + " ],\n", + " \"https://synbiohub.org\",\n", + " auth,\n", + " sbol2.Document(),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "99d093a8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[Plasmid:\n", + " Name: pB0033_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0033_BC/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['B', 'C']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: DVA_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_CD/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['C', 'D']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_AB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_AB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_GB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: DVA_DH_D_H\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_DH/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'H']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DE_D_E\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DE/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'E']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_GB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DH_D_H\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DH/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'H']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: DVA_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_AB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DG_D_G\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DG/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'G']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pE1010_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE1010_CD/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['C', 'D']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pE0040_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0040_CD/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['C', 'D']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_EB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['E', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0032_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0032_BC/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['B', 'C']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: DVA_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: DVA_DG_D_G\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_DG/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'G']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pE0030_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0030_CD/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['C', 'D']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23100_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: DVA_DF_D_F\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_DF/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'F']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_EB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['E', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0015_DF_D_F\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DF/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['D', 'F']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_EB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['E', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: DVA_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_BC/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['B', 'C']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pB0034_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0034_BC/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['B', 'C']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23116_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_AB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['A', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_GB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: DVA_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_GB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['G', 'B']\n", + " Antibiotic Resistance: Ampicillin\n", + "]\n" + ] + } + ], + "source": [ + "print(buildcompiler.indexed_plasmids)" + ] + }, + { + "cell_type": "markdown", + "id": "eb4a9ab0", + "metadata": {}, + "source": [ + "# Create Collection of Implementations To Test" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "9cdd18d0", + "metadata": {}, + "outputs": [], + "source": [ + "implementation_collection = sbol2.Document()\n", + "plas_doc = sbol2.Document()\n", + "\n", + "buildcompiler.sbh.pull(\n", + " \"https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/CIDARMoCloPlasmidsKit_collection/1\",\n", + " plas_doc,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "d76dbdeb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "amp_region\n", + "B0033\n", + "J23106\n", + "DVK_AE\n", + "E0030_yfp\n", + "kan_region\n", + "E0040m_gfp\n", + "B0015\n", + "J23116\n", + "DVA_AF\n", + "J23100\n", + "DVA_AF2\n", + "DVK_GH\n", + "Fusion_Site_E\n", + "Fusion_Site_F\n", + "Fusion_Site_D\n", + "Fusion_Site_G\n", + "E1010m_rfp\n", + "Fusion_Site_A\n", + "DVK_FG\n", + "LacZ_cassette\n", + "Fusion_Site_B\n", + "Fusion_Site_C\n", + "LacZ_cassette2\n", + "Fusion_Site_H\n", + "B0032\n", + "B0034\n", + "dva_backbone_core\n", + "origin_of_replication_pSB1A2\n", + "DVK_EF\n", + "dvk_backbone_core\n", + "Design........................0\n", + "Build.........................0\n", + "Test..........................0\n", + "Analysis......................0\n", + "ComponentDefinition...........0\n", + "ModuleDefinition..............0\n", + "Model.........................0\n", + "Sequence......................0\n", + "Collection....................0\n", + "Activity......................0\n", + "Plan..........................0\n", + "Agent.........................0\n", + "Attachment....................0\n", + "CombinatorialDerivation.......0\n", + "Implementation................30\n", + "SampleRoster..................0\n", + "Experiment....................0\n", + "ExperimentalData..............0\n", + "Annotation Objects............0\n", + "---\n", + "Total: .........................30\n", + "\n" + ] + } + ], + "source": [ + "implementation_collection.default_namespace = (\n", + " \"http://buildcompiler.org/implementations/\"\n", + ")\n", + "\n", + "dummy_activity = sbol2.Activity(\"plasmid_dna_extraction\")\n", + "dummy_activity.name = \"DNA extraction\"\n", + "dummy_activity.types = \"http://sbols.org/v2#build\"\n", + "\n", + "for plasmid in plas_doc.componentDefinitions:\n", + " if \"http://identifiers.org/so/SO:0000637\" in plasmid.roles:\n", + " implementation = sbol2.Implementation(f\"{plasmid.displayId}_impl\")\n", + " implementation.built = plasmid.identity\n", + " implementation.wasGeneratedBy = dummy_activity\n", + "\n", + " implementation_collection.add(implementation)\n", + " else:\n", + " print(plasmid.displayId)\n", + "\n", + "print(implementation_collection)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "3bcf02fb", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Valid.'" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "implementation_collection.write(\"moclo_implementations.xml\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "9bf9c396", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['pB0033_BC_impl',\n", + " 'DVA_CD_impl',\n", + " 'pJ23100_AB_impl',\n", + " 'pJ23106_AB_impl',\n", + " 'pJ23116_GB_impl',\n", + " 'DVA_DH_impl',\n", + " 'pB0015_DE_impl',\n", + " 'pJ23100_GB_impl',\n", + " 'pB0015_DH_impl',\n", + " 'DVA_AB_impl',\n", + " 'pJ23116_FB_impl',\n", + " 'pB0015_DG_impl',\n", + " 'pE1010_CD_impl',\n", + " 'pE0040_CD_impl',\n", + " 'pJ23100_EB_impl',\n", + " 'pB0032_BC_impl',\n", + " 'DVA_FB_impl',\n", + " 'DVA_DG_impl',\n", + " 'pE0030_CD_impl',\n", + " 'pJ23100_FB_impl',\n", + " 'DVA_DF_impl',\n", + " 'pJ23106_EB_impl',\n", + " 'pB0015_DF_impl',\n", + " 'pJ23116_EB_impl',\n", + " 'pJ23106_FB_impl',\n", + " 'DVA_BC_impl',\n", + " 'pB0034_BC_impl',\n", + " 'pJ23116_AB_impl',\n", + " 'pJ23106_GB_impl',\n", + " 'DVA_GB_impl']" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[cd.displayId for cd in implementation_collection.implementations]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d7b8369e", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python (buildplanner)", + "language": "python", + "name": "buildplanner" + }, + "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.13.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 767fa6bdfecd8e7409e2f753969dfb16e504381b Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Fri, 15 May 2026 23:12:49 -0600 Subject: [PATCH 83/93] decouple forward and reverse fusion site matching to fix skipped backbone match + composite numbering adjustment --- src/buildcompiler/sbol2build.py | 40 +++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/buildcompiler/sbol2build.py b/src/buildcompiler/sbol2build.py index 4ad1312..8bc8266 100644 --- a/src/buildcompiler/sbol2build.py +++ b/src/buildcompiler/sbol2build.py @@ -904,6 +904,14 @@ def ligation( for part in remaining_parts: # match insert sequence 5' to part 3' part_sequence_uri = part.sequences[0] + # check reverse match + if ( + source_document.getSequence(part_sequence_uri) + .elements[-fusion_site_length:] + .lower() + == insert_sequence[:fusion_site_length].lower() + ): + insert_3prime_match_id = part.identity if ( source_document.getSequence(part_sequence_uri) .elements[:fusion_site_length] @@ -928,13 +936,6 @@ def ligation( list_of_parts_per_composite.append(part) remaining_parts.remove(part) # match backbone 5' to insert sequence 3', set flag - elif ( - source_document.getSequence(part_sequence_uri) - .elements[-fusion_site_length:] - .lower() - == insert_sequence[:fusion_site_length].lower() - ): - insert_3prime_match_id = part.identity remaining_parts_after = len(remaining_parts) if remaining_parts_before == remaining_parts_after: @@ -948,9 +949,8 @@ def ligation( # transform list_of_parts_per_assembly into list of composites product_impl_list = [] - composite_number = 1 + composite_number = 0 - # TODO: use componentinstances to append "subcomponents" to each definition that is a composite component. all composites share the "subcomponents" for composite in list_of_composites_per_assembly: # a composite of the form [A,B,C] # calculate sequence composite_sequence_str = "" @@ -1027,15 +1027,25 @@ def ligation( source_document.getComponentDefinition(prev_three_prime) ) else: + anno_prefix = comp.displayId + + matching_anno_prefix = [ + a.displayId + for a in anno_list + if a.displayId.startswith(f"{comp.displayId}_") + ] + if matching_anno_prefix: + anno_prefix = f"{comp.displayId}_{len(matching_anno_prefix)}" + temp_extract_components.append(comp.definition) comp_location = sbol2.Range( - uri=f"{comp.displayId}_location", + uri=f"{anno_prefix}_location", start=len(composite_sequence_str) + fusion_site_length + 1, end=len(composite_sequence_str) + len(part_extract_sequence[:-4]), - ) # TODO check if seq len is correct + ) comp_anno = sbol2.SequenceAnnotation( - uri=f"{comp.displayId}_annotation" + uri=f"{anno_prefix}_annotation" ) comp_anno.locations.add(comp_location) anno_list.append(comp_anno) @@ -1046,15 +1056,17 @@ def ligation( composite_sequence_str + part_extract_sequence[:-fusion_site_length] ) # needs a version for linear + suffix = f"_{composite_number}" if composite_number > 0 else "" + # create dna component and sequence composite_component_definition, composite_seq = ( dna_componentdefinition_with_sequence( - f"{composite_prefix}_{composite_number}", + f"{composite_prefix}{suffix}", composite_sequence_str, molecule=True, ) ) - composite_component_definition.name = f"{composite_prefix}_{composite_number}" + composite_component_definition.name = f"{composite_prefix}{suffix}" composite_component_definition.addRole(ENGINEERED_PLASMID) composite_component_definition.addType(CIRCULAR) From 0454cfe4cc3cde910b5adbb9c704e3c73d0dbb9d Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Fri, 15 May 2026 23:13:47 -0600 Subject: [PATCH 84/93] lvl2 working --- notebooks/build_compiler_test.ipynb | 355 ++++++---------------------- 1 file changed, 68 insertions(+), 287 deletions(-) diff --git a/notebooks/build_compiler_test.ipynb b/notebooks/build_compiler_test.ipynb index 62a0cbe..f1e5a8f 100644 --- a/notebooks/build_compiler_test.ipynb +++ b/notebooks/build_compiler_test.ipynb @@ -30,7 +30,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "['i6TQvNp91', 'i0mwvNcgH']\n" + "['qlSBuNBL', 'i0mwvNcgH']\n" ] } ], @@ -64,16 +64,12 @@ "output_type": "stream", "text": [ "Indexing collection: https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\n", - "Indexing collection: https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\n", - "['http://rebase.neb.com/rebase/enz/BsaI.html'] ['http://identifiers.org/obi/OBI_0000732']\n", - "[] ['http://identifiers.org/ncit/NCIT:C16796']\n", - "['http://rebase.neb.com/rebase/enz/BbsI.html'] ['http://identifiers.org/obi/OBI_0000732']\n", - "['http://rebase.neb.com/rebase/enz/SapI.html'] ['http://identifiers.org/obi/OBI_0000732']\n" + "Indexing collection: https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\n" ] } ], "source": [ - "auth = \"ca97f26e-9d33-4e38-810d-04d99f36e47c\"\n", + "auth = \"c6f22554-f057-4b4f-a597-67041d9002e5\"\n", "collections = [\n", " \"https://synbiohub.org/user/Gon/impl_test/impl_test_collection/1\",\n", " \"https://synbiohub.org/user/Gon/Enzyme_Implementations/Enzyme_Implementations_collection/1\",\n", @@ -101,7 +97,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "id": "6ec9e2fe", "metadata": {}, "outputs": [ @@ -109,54 +105,33 @@ "name": "stdout", "output_type": "stream", "text": [ - "matched pJ23100_AB_A_B with DVK_AE_A_E on fusion site A!\n", - "matched pB0034_BC_B_C with pJ23100_AB_A_B on fusion site B!\n", - "matched pE0030_CD_C_D with pB0034_BC_B_C on fusion site C!\n", - "matched final component pB0015_DE_D_E with pE0030_CD_C_D and DVK_AE_A_E on fusion sites (D, E)!\n", - "Success with backbone: DVK_AE_A_E and plasmids: ['pJ23100_AB_A_B', 'pB0034_BC_B_C', 'pE0030_CD_C_D', 'pB0015_DE_D_E']\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/ryan/GitRepo/SBOL2Build/src/buildcompiler/buildcompiler.py:321: RuntimeWarning: BsaI Restriction enzyme not found in provided collection(s). Domestication via purchase will be added to protocol.\n", - " warnings.warn(\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "matched pJ23116_AB_A_B with DVK_AE_A_E on fusion site A!\n", - "matched pB0034_BC_B_C with pJ23116_AB_A_B on fusion site B!\n", - "matched pE0030_CD_C_D with pB0034_BC_B_C on fusion site C!\n", - "matched final component pB0015_DE_D_E with pE0030_CD_C_D and DVK_AE_A_E on fusion sites (D, E)!\n" + "Success with backbone: DVK_AE_A_E and plasmids: ['pJ23100_AB_A_B', 'pB0034_BC_B_C', 'pE0030_CD_C_D', 'pB0015_DE_D_E']\n", + "Success with backbone: DVK_AE_A_E and plasmids: ['pJ23116_AB_A_B', 'pB0034_BC_B_C', 'pE0030_CD_C_D', 'pB0015_DE_D_E']\n" ] }, { "data": { "text/plain": [ - "({'https://sbolcanvas.org/i6TQvNp91/1': [Plasmid:\n", - " Name: i6TQvNp91_composite_1_A_E\n", - " Plasmid Definition: https://SBOL2Build.org/i6TQvNp91_composite_1/1\n", + "({'https://sbolcanvas.org/qlSBuNBL/1': [Plasmid:\n", + " Name: qlSBuNBL_composite_1_A_E\n", + " Plasmid Definition: http://buildcompiler.org/qlSBuNBL_composite_1/1\n", " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://SBOL2Build.org/i6TQvNp91_composite_1_impl/1']\n", + " Plasmid Implementations: ['http://buildcompiler.org/qlSBuNBL_composite_1_impl/1']\n", " Strain Implementations: [None]\n", " Fusion Sites: ['A', 'E']\n", " Antibiotic Resistance: Kanamycin],\n", " 'https://sbolcanvas.org/i0mwvNcgH/1': [Plasmid:\n", " Name: i0mwvNcgH_composite_1_A_E\n", - " Plasmid Definition: https://SBOL2Build.org/i0mwvNcgH_composite_1/1\n", + " Plasmid Definition: http://buildcompiler.org/i0mwvNcgH_composite_1/1\n", " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://SBOL2Build.org/i0mwvNcgH_composite_1_impl/1']\n", + " Plasmid Implementations: ['http://buildcompiler.org/i0mwvNcgH_composite_1_impl/1']\n", " Strain Implementations: [None]\n", " Fusion Sites: ['A', 'E']\n", " Antibiotic Resistance: Kanamycin]},\n", - " )" + " )" ] }, - "execution_count": 4, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -180,290 +155,96 @@ "execution_count": null, "id": "3e2272ff", "metadata": {}, - "outputs": [ - { - "ename": "NameError", - "evalue": "name 'assembly_lvl2' is not defined", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mNameError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[5]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m composite_plasmids, final_doc = \u001b[43massembly_lvl2\u001b[49m(buildcompiler, design_doc)\n", - "\u001b[31mNameError\u001b[39m: name 'assembly_lvl2' is not defined" - ] - } - ], - "source": [ - "composite_plasmids, final_doc = buildcompiler.assembly_lvl2(None)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2ee945ec", - "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[Plasmid:\n", - " Name: pE0040_CD_C_D\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0040_CD/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pE0040_CD_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['C', 'D']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pB0034_BC_B_C\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0034_BC/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0034_BC_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['B', 'C']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pJ23100_EB_E_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_EB/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23100_EB_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['E', 'B']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pJ23100_AB_A_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_AB/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23100_AB_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['A', 'B']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pB0033_BC_B_C\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0033_BC/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0033_BC_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['B', 'C']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pJ23106_FB_F_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_FB/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23106_FB_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['F', 'B']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pJ23116_EB_E_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_EB/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23116_EB_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['E', 'B']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pJ23116_GB_G_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_GB/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23116_GB_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['G', 'B']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pE0030_CD_C_D\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0030_CD/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pE0030_CD_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['C', 'D']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pJ23100_FB_F_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_FB/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23100_FB_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['F', 'B']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pJ23116_AB_A_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_AB/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23116_AB_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['A', 'B']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pB0032_BC_B_C\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0032_BC/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0032_BC_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['B', 'C']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pB0015_DF_D_F\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DF/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0015_DF_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['D', 'F']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pB0015_DG_D_G\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DG/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0015_DG_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['D', 'G']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pB0015_DH_D_H\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DH/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0015_DH_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['D', 'H']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pJ23106_AB_A_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_AB/1\n", + "Gen\n", + "Gen1\n", + "Plasmid:\n", + " Name: Gen_Gen1_plas_1_simple_A_E\n", + " Plasmid Definition: http://buildcompiler.org/Gen_Gen1_plas_1_simple/1\n", " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23106_AB_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['A', 'B']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pJ23106_GB_G_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_GB/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23106_GB_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['G', 'B']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pJ23100_GB_G_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_GB/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23100_GB_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['G', 'B']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pE1010_CD_C_D\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE1010_CD/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pE1010_CD_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['C', 'D']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pB0015_DE_D_E\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DE/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pB0015_DE_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['D', 'E']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pJ23116_FB_F_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_FB/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23116_FB_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['F', 'B']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pJ23106_EB_E_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_EB/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/pJ23106_EB_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['E', 'B']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: Gen_plas_1_A_E\n", - " Plasmid Definition: https://SBOL2Build.org/Gen_plas_1/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://SBOL2Build.org/Gen_plas_1_impl/1']\n", + " Plasmid Implementations: ['http://buildcompiler.org/Gen_Gen1_plas_1_impl/1']\n", " Strain Implementations: [None]\n", " Fusion Sites: ['A', 'E']\n", " Antibiotic Resistance: Kanamycin\n", - ", Plasmid:\n", - " Name: Gen1_plas_1_E_F\n", - " Plasmid Definition: https://SBOL2Build.org/Gen1_plas_1/1\n", + "\n", + "Plasmid:\n", + " Name: Gen1_Gen1_plas_1_simple_E_F\n", + " Plasmid Definition: http://buildcompiler.org/Gen1_Gen1_plas_1_simple/1\n", " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://SBOL2Build.org/Gen1_plas_1_impl/1']\n", + " Plasmid Implementations: ['http://buildcompiler.org/Gen1_Gen1_plas_1_impl/1']\n", " Strain Implementations: [None]\n", " Fusion Sites: ['E', 'F']\n", " Antibiotic Resistance: Kanamycin\n", - "] [Plasmid:\n", - " Name: DVK_EF_E_F\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_EF/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/DVK_EF_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['E', 'F']\n", - " Antibiotic Resistance: Kanamycin\n", - ", Plasmid:\n", - " Name: DVK_AE_A_E\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_AE/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/DVK_AE_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['A', 'E']\n", - " Antibiotic Resistance: Kanamycin\n", - ", Plasmid:\n", - " Name: DVK_FG_F_G\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_FG/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/DVK_FG_impl/1']\n", - " Strain Implementations: None\n", - " Fusion Sites: ['F', 'G']\n", - " Antibiotic Resistance: Kanamycin\n", - ", Plasmid:\n", - " Name: DVK_GH_G_H\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVK_GH/1\n", + "\n", + "Success with backbone: DVA_AF2_A_F and plasmids: ['Gen_Gen1_plas_1_simple_A_E', 'Gen1_Gen1_plas_1_simple_E_F']\n", + "Plasmid:\n", + " Name: DVA_AF2_A_F\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_AF2/1\n", " Strain Definitions: [None]\n", - " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/DVK_GH_impl/1']\n", + " Plasmid Implementations: ['https://synbiohub.org/user/Gon/impl_test/DVA_AF2_impl/1']\n", " Strain Implementations: None\n", - " Fusion Sites: ['G', 'H']\n", - " Antibiotic Resistance: Kanamycin\n", - "]\n" + " Fusion Sites: ['A', 'F']\n", + " Antibiotic Resistance: Ampicillin\n", + "\n", + "Comparing gctt (Gen1_Gen1_plas_1_simple_extracted_part)to gcttand reverse: cgct (Gen1_Gen1_plas_1_simple_extracted_part)to ggag\n", + "Comparing cgct (DVA_AF2_extracted_backbone)to cgctand reverse: ggag (DVA_AF2_extracted_backbone)to ggag\n" ] } ], "source": [ - "print(buildcompiler.indexed_plasmids, buildcompiler.indexed_backbones)" + "lvl2_design_doc = sbol2.Document()\n", + "lvl2_design_doc.read(\"../tests/test_files/ExampleLvl2_design.xml\")\n", + "\n", + "\n", + "composite_plasmids, final_doc = buildcompiler.assembly_lvl2(\n", + " lvl2_design_doc, product_name=\"lvl2\"\n", + ")" ] }, { "cell_type": "code", - "execution_count": null, - "id": "7c12e504", + "execution_count": 7, + "id": "ba31c4c6", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[, ]\n", - "[]\n" + "[Plasmid:\n", + " Name: lvl2_1_A_F\n", + " Plasmid Definition: http://buildcompiler.org/lvl2_1/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: ['http://buildcompiler.org/lvl2_1_impl/1']\n", + " Strain Implementations: [None]\n", + " Fusion Sites: ['A', 'F']\n", + " Antibiotic Resistance: Ampicillin\n", + "]\n" ] + }, + { + "data": { + "text/plain": [ + "'Valid.'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "print(buildcompiler.restriction_enzyme_implementations)\n", - "print(buildcompiler.ligase_implementations)\n", - "\n", - "# composite_plasmids = buildcompiler.assembly_lvl1(design, None)" + "print(composite_plasmids)\n", + "final_doc.write(\"lvl2_assembly.xml\")" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "79fd0cb5", "metadata": {}, "outputs": [], @@ -495,7 +276,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "8f4ea67c", "metadata": {}, "outputs": [], From 307f61c48b988f40be97cb1ae0f3312f8fa59f53 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Sun, 17 May 2026 17:55:56 -0600 Subject: [PATCH 85/93] lvl2 test draft --- tests/test_buildcompiler.py | 173 +++++++++++++++++++++++++++++++++++- 1 file changed, 171 insertions(+), 2 deletions(-) diff --git a/tests/test_buildcompiler.py b/tests/test_buildcompiler.py index c407c5b..b843754 100644 --- a/tests/test_buildcompiler.py +++ b/tests/test_buildcompiler.py @@ -107,7 +107,7 @@ def test_simple_lvl1_assembly(self): f"Implementation {impl.identity} should reference a built object", ) - expected_product_uri = "http://buildcompiler.org/qlSBuNBL_composite_1_impl/1" + expected_product_uri = "http://buildcompiler.org/qlSBuNBL_composite_impl/1" product_impl = product_doc.get(expected_product_uri) product_def = product_doc.get(product_impl.built) @@ -125,7 +125,7 @@ def test_simple_lvl1_assembly(self): self.assertEqual( product_def.identity, - "http://buildcompiler.org/qlSBuNBL_composite_1/1", + "http://buildcompiler.org/qlSBuNBL_composite/1", f"Implementation {product_impl} must build {product_def}", ) @@ -253,6 +253,175 @@ def test_complex_combinatorial_translation( f"Expected B0033 to appear 3 times across the composite dictionary, found {rbs_counts['B0033']}", ) + def test_simple_lvl2_assembly(self): + """ + High-level integration test for lvl2 assembly. + + Validates: + - lvl2 plasmid generation succeeds + - lvl1 intermediates are used as assembly inputs + - correct enzymes are included + - correct backbone selection occurs + - TU ordering is preserved + - SBOL provenance relationships are intact + """ + + abstract_design_doc = sbol2.Document() + abstract_design_doc.read("tests/test_files/ExampleLvl2_design.xml") + + # ------------------------------------------------------------ + # Run lvl2 assembly + # ------------------------------------------------------------ + lvl2_plasmids, final_doc = self.buildcompiler.assembly_lvl2( + abstract_design_doc, + product_name="lvl2", + ) + + self.assertEqual( + len(lvl2_plasmids), + 1, + "Expected exactly one lvl2 plasmid to be produced", + ) + + lvl2_plasmid = lvl2_plasmids[0] + + # ------------------------------------------------------------ + # Validate provenance / implementation relationships + # ------------------------------------------------------------ + product_impl = lvl2_plasmid.plasmid_implementations[0] + product_def = lvl2_plasmid.plasmid_definition + + self.assertIsNotNone( + product_impl, + "Lvl2 plasmid implementation should exist", + ) + + self.assertIsNotNone( + product_def, + "Lvl2 plasmid definition should exist", + ) + + self.assertEqual( + product_impl.built, + product_def.identity, + "Implementation should reference the built lvl2 construct", + ) + + # test level 1 assembly activities + lvl1_activities = [ + "http://buildcompiler.org/Gen1_Gen1_plas_assembly/1", + "http://buildcompiler.org/Gen_Gen1_plas_assembly/1", + ] + + lvl1_activities + + # TODO add same tests for lvl1 as with the level 2 activities + + # test level2 assembly activity + lvl2_assembly_activity = final_doc.get( + "http://buildcompiler.org/lvl2_assembly/1" + ) + + self.assertIsNotNone( + lvl2_assembly_activity, + "Assembly activity should exist in final document", + ) + + self.assertEqual( + product_impl.wasGeneratedBy, + [lvl2_assembly_activity.identity], + "Lvl2 implementation should reference generating assembly activity", + ) + + # ------------------------------------------------------------ + # Validate expected assembly usages + # ------------------------------------------------------------ + usage_entities = [ + get_or_pull(final_doc, self.buildcompiler.sbh, u.entity, True) + for u in lvl2_assembly_activity.usages + ] + + usage_display_ids = { + entity.displayId for entity in usage_entities if entity is not None + } + + self.assertIn( + "BbsI_impl", + usage_display_ids, + "Lvl2 assembly should use BbsI", + ) + + self.assertIn( + "T4_Ligase_impl", + usage_display_ids, + "Lvl2 assembly should use T4 ligase", + ) + + # ------------------------------------------------------------ + # Ensure lvl1 plasmids were used as inputs + # ------------------------------------------------------------ + lvl1_inputs = [ + entity + for entity in usage_entities + if entity is not None and "_plas" in entity.displayId.lower() + ] + + self.assertEqual( + len(lvl1_inputs), + 2, + "Lvl2 assembly should consume 2 lvl1 plasmids as inputs", + ) + + # ------------------------------------------------------------ + # Validate final construct ordering + # ------------------------------------------------------------ + components = product_def.getInSequentialOrder() + display_ids = [component.displayId for component in components] + + expected_order = [ + "Ligation_Scar_A", + "Gen_Gen1_plas_TU", + "Ligation_Scar_E", + "Gen1_Gen1_plas_TU", + "Ligation_Scar_F", + "dva_backbone_core", + ] + + self.assertEqual( + display_ids, + expected_order, + "Lvl2 construct does not preserve expected TU ordering", + ) + + # ------------------------------------------------------------ + # Validate all usage entities are valid implementations + # ------------------------------------------------------------ + for entity in usage_entities: + self.assertIsNotNone( + entity, + "All assembly usage entities should resolve correctly", + ) + + self.assertIsInstance( + entity, + sbol2.Implementation, + "Assembly usages should reference SBOL implementations", + ) + + self.assertIsNotNone( + entity.built, + f"Implementation {entity.identity} should reference a built object", + ) + + # Ensure no duplicate lvl1 intermediates were generated + lvl1_ids = [entity.displayId for entity in lvl1_inputs] + + self.assertEqual( + len(lvl1_ids), + len(set(lvl1_ids)), + "Duplicate lvl1 intermediates detected", + ) + if __name__ == "__main__": unittest.main() From 9cebf0ddc5957d6796058a3954060df5e57e7e93 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Sun, 17 May 2026 18:39:49 -0600 Subject: [PATCH 86/93] lvl1 TU tests for lvl2 assembly --- tests/test_buildcompiler.py | 73 +++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/tests/test_buildcompiler.py b/tests/test_buildcompiler.py index b843754..3aca313 100644 --- a/tests/test_buildcompiler.py +++ b/tests/test_buildcompiler.py @@ -6,7 +6,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) -from buildcompiler.buildcompiler import BuildCompiler +from buildcompiler.buildcompiler import BuildCompiler, _extract_lvl2_TUs from buildcompiler.abstract_translator import extract_toplevel_definition, get_or_pull @@ -269,6 +269,8 @@ def test_simple_lvl2_assembly(self): abstract_design_doc = sbol2.Document() abstract_design_doc.read("tests/test_files/ExampleLvl2_design.xml") + design_TUs = _extract_lvl2_TUs(abstract_design_doc) + # ------------------------------------------------------------ # Run lvl2 assembly # ------------------------------------------------------------ @@ -309,13 +311,76 @@ def test_simple_lvl2_assembly(self): # test level 1 assembly activities lvl1_activities = [ - "http://buildcompiler.org/Gen1_Gen1_plas_assembly/1", "http://buildcompiler.org/Gen_Gen1_plas_assembly/1", + "http://buildcompiler.org/Gen1_Gen1_plas_assembly/1", ] - lvl1_activities + for index, activity_id in enumerate(lvl1_activities): + lvl1_activity = final_doc.get(activity_id) + + self.assertIsNotNone( + lvl1_activity, + f"Lvl1 assembly activity {activity_id} should exist", + ) + + # ------------------------------------------------------------ + # Validate lvl1 activity usages + # ------------------------------------------------------------ + lvl1_usage_entities = [ + get_or_pull(final_doc, self.buildcompiler.sbh, u.entity, True) + for u in lvl1_activity.usages + ] + + lvl1_usage_display_ids = { + entity.displayId for entity in lvl1_usage_entities if entity is not None + } + + lvl1_usage_built_CDs = { + final_doc.get(entity.built) + for entity in lvl1_usage_entities + if entity is not None + } + + for comp in design_TUs[index].components: + self.assertIn( + comp.definition, + [ + subcomp.definition + for plas in lvl1_usage_built_CDs + for subcomp in plas.components + ], + f"Level 1 assembly activity {activity_id} missing {comp.definition} from design TU {design_TUs[index]}", + ) - # TODO add same tests for lvl1 as with the level 2 activities + self.assertIn( + "BsaI_impl", + lvl1_usage_display_ids, + "Lvl1 assembly should use BsaI", + ) + + self.assertIn( + "T4_Ligase_impl", + lvl1_usage_display_ids, + "Lvl1 assembly should use T4 ligase", + ) + + for entity in lvl1_usage_entities: + self.assertIsNotNone( + entity, + "All lvl1 assembly usage entities should resolve correctly", + ) + + self.assertIsInstance( + entity, + sbol2.Implementation, + "Lvl1 assembly usages should reference SBOL implementations", + ) + + self.assertGreaterEqual( + len(lvl1_usage_entities), + 3, + "Lvl1 assembly should contain enzyme and plasmid usages", + ) # test level2 assembly activity lvl2_assembly_activity = final_doc.get( From 769bd06c4f1a12c79d6ff15494fb0658f9b85671 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Mon, 18 May 2026 09:33:02 -0600 Subject: [PATCH 87/93] Fix local document indexing to avoid duplicate re-indexing --- src/buildcompiler/buildcompiler.py | 33 ++++++++++++++++++++++-------- tests/test_buildcompiler.py | 27 +++++++++++++++++++++++- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index 57b4b58..47a1838 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -101,7 +101,7 @@ def from_local_documents( def index_document(self, collection_doc: sbol2.Document): self._merge_document(collection_doc) - self._index_current_document() + self._index_document_objects(collection_doc) def _index_collections(self, collections: List[str]): """Index input collections into plasmids and backbones. @@ -144,7 +144,14 @@ def _resolve_object(self, uri: str): return get_or_pull(self.sbol_doc, self.sbh, uri) def _index_current_document(self): - for implementation in self.sbol_doc.implementations: + self._index_document_objects(self.sbol_doc) + + def _append_implementation_once(self, implementations: list, implementation): + if not any(existing.identity == implementation.identity for existing in implementations): + implementations.append(implementation) + + def _index_document_objects(self, source_doc: sbol2.Document): + for implementation in source_doc.implementations: built_object = self._resolve_object(implementation.built) if ( type(built_object) is sbol2.ModuleDefinition @@ -162,7 +169,9 @@ def _index_current_document(self): self.indexed_plasmids, built_object ) if existing_plasmid: - existing_plasmid.plasmid_implementations.append(implementation) + self._append_implementation_once( + existing_plasmid.plasmid_implementations, implementation + ) else: self.indexed_plasmids.append( Plasmid( @@ -174,7 +183,9 @@ def _index_current_document(self): self.indexed_backbones, built_object ) if existing_backbone: - existing_backbone.plasmid_implementations.append(implementation) + self._append_implementation_once( + existing_backbone.plasmid_implementations, implementation + ) else: self.indexed_backbones.append( Plasmid( @@ -183,15 +194,19 @@ def _index_current_document(self): ) elif sbol2.BIOPAX_PROTEIN in built_object.types: if RESTRICTION_ENZYME in built_object.roles: - self.restriction_enzyme_implementations.append(implementation) + self._append_implementation_once( + self.restriction_enzyme_implementations, implementation + ) elif LIGASE in built_object.roles: - self.ligase_implementations.append(implementation) + self._append_implementation_once( + self.ligase_implementations, implementation + ) - for strain in self.sbol_doc.moduleDefinitions: + for strain in source_doc.moduleDefinitions: if ORGANISM_STRAIN in strain.roles: self._extract_plasmids_from_strain(strain, None, self.sbol_doc) - for definition in self.sbol_doc.componentDefinitions: + for definition in source_doc.componentDefinitions: self._sort_plasmid_components(definition, self.sbol_doc) def domestication( @@ -818,7 +833,7 @@ def _extract_design_parts( ] def _get_abstract_design(self) -> sbol2.ComponentDefinition: - for definition in self.sbol_doc.componentDefinitions: + for definition in source_doc.componentDefinitions: if ( ENGINEERED_PLASMID in definition.roles or PLASMID_CLONING_VECTOR in definition.roles diff --git a/tests/test_buildcompiler.py b/tests/test_buildcompiler.py index 4acd224..65f124d 100644 --- a/tests/test_buildcompiler.py +++ b/tests/test_buildcompiler.py @@ -9,7 +9,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src'))) from buildcompiler.buildcompiler import BuildCompiler -from buildcompiler.constants import RESTRICTION_ENZYME +from buildcompiler.constants import LIGASE, RESTRICTION_ENZYME class TestBuildCompilerLocalIndexing(unittest.TestCase): @@ -40,6 +40,31 @@ def test_from_local_documents_indexes_without_partshop(self): self.assertIsInstance(compiler.restriction_enzyme_implementations, list) self.assertIsInstance(compiler.ligase_implementations, list) + + def test_from_local_documents_does_not_reindex_prior_documents(self): + restriction_doc = sbol2.Document() + enzyme = sbol2.ComponentDefinition('BsaI') + enzyme.types = [sbol2.BIOPAX_PROTEIN] + enzyme.roles = [RESTRICTION_ENZYME] + restriction_doc.add(enzyme) + restriction_impl = sbol2.Implementation('BsaI_impl') + restriction_impl.built = enzyme.identity + restriction_doc.add(restriction_impl) + + ligase_doc = sbol2.Document() + ligase = sbol2.ComponentDefinition('T4Ligase') + ligase.types = [sbol2.BIOPAX_PROTEIN] + ligase.roles = [LIGASE] + ligase_doc.add(ligase) + ligase_impl = sbol2.Implementation('T4Ligase_impl') + ligase_impl.built = ligase.identity + ligase_doc.add(ligase_impl) + + compiler = BuildCompiler.from_local_documents([restriction_doc, ligase_doc]) + + self.assertEqual(len(compiler.restriction_enzyme_implementations), 1) + self.assertEqual(len(compiler.ligase_implementations), 1) + def test_local_mode_raises_when_reference_missing(self): doc = sbol2.Document() strain = sbol2.ModuleDefinition('strain_missing_ref') From 207b68c3f2547e450e022b8ab9549c8c543edd6a Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Mon, 18 May 2026 11:59:52 -0600 Subject: [PATCH 88/93] transformation test --- tests/test_buildcompiler.py | 190 ++ tests/test_files/DVK_AE.xml | 443 ++++ tests/test_files/ExampleLvl2_design.xml | 593 ++++++ tests/test_files/comp_tu.xml | 343 +++ tests/test_files/mocloparts116.xml | 307 +++ tests/test_files/pB0015_DE.xml | 440 ++++ tests/test_files/pB0032_BC.xml | 440 ++++ tests/test_files/pE0040_CD.xml | 440 ++++ tests/test_files/pJ23100_AB.xml | 44 +- tests/test_files/transformation_activity.xml | 1884 +++++++++++++++++ .../transformation_activity_new.xml | 1884 +++++++++++++++++ 11 files changed, 7005 insertions(+), 3 deletions(-) create mode 100644 tests/test_files/DVK_AE.xml create mode 100644 tests/test_files/ExampleLvl2_design.xml create mode 100644 tests/test_files/comp_tu.xml create mode 100644 tests/test_files/mocloparts116.xml create mode 100644 tests/test_files/pB0015_DE.xml create mode 100644 tests/test_files/pB0032_BC.xml create mode 100644 tests/test_files/pE0040_CD.xml create mode 100644 tests/test_files/transformation_activity.xml create mode 100644 tests/test_files/transformation_activity_new.xml diff --git a/tests/test_buildcompiler.py b/tests/test_buildcompiler.py index 3aca313..4075e2e 100644 --- a/tests/test_buildcompiler.py +++ b/tests/test_buildcompiler.py @@ -2,8 +2,10 @@ import unittest import sys import os +import copy from collections import Counter + sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../src"))) from buildcompiler.buildcompiler import BuildCompiler, _extract_lvl2_TUs @@ -487,6 +489,194 @@ def test_simple_lvl2_assembly(self): "Duplicate lvl1 intermediates detected", ) + def test_transformation(self): + transformation_doc = sbol2.Document() + + chassis_md = sbol2.ModuleDefinition("E_coli_DH5alpha") + chassis_impl = sbol2.Implementation("E_coli_DH5alpha_impl") + chassis_impl.built = chassis_md.identity + + transformation_doc.add(chassis_md) + transformation_doc.add(chassis_impl) + + result = self.buildcompiler.transformation( + assembly_products=self.buildcompiler.indexed_plasmids[:2], + chassis_name="E_coli_DH5alpha", + transformation_doc=transformation_doc, + ) + + # Top-level output structure + self.assertEqual(result["stage"], "transformation") + + plasmid_displayIds = [ + plasmid.plasmid_definition.displayId + for plasmid in self.buildcompiler.indexed_plasmids[:2] + ] + + self.assertEqual( + result["inputs"], + plasmid_displayIds, + ) + + self.assertEqual( + result["chassis"], + "E_coli_DH5alpha", + ) + + self.assertIn("sbol_artifacts", result) + self.assertIn("json_intermediate", result) + self.assertIn("protocol_artifacts", result) + + # Robot JSON intermediate + json_intermediate = result["json_intermediate"] + + self.assertEqual( + json_intermediate["protocol"], + "chemical_transformation", + ) + + self.assertEqual( + json_intermediate["version"], + "0.1", + ) + + self.assertEqual(len(json_intermediate["steps"]), 2) + + first_step = json_intermediate["steps"][0] + + self.assertEqual(first_step["step"], 1) + + self.assertEqual(first_step["plasmid"], plasmid_displayIds[0]) + + self.assertEqual( + first_step["heat_shock"], + { + "temperature_c": 42, + "duration_seconds": 45, + }, + ) + + # SBOL artifact outputs + sbol_artifacts = result["sbol_artifacts"] + + self.assertEqual(len(sbol_artifacts), 2) + + first_artifact = sbol_artifacts[0] + + self.assertIn("transformation_activity", first_artifact) + + self.assertIn( + "transformed_strain_module", + first_artifact, + ) + + self.assertIn( + "transformed_strain_implementation", + first_artifact, + ) + + # Verify generated SBOL objects exist in document + transform_activity = transformation_doc.get( + f"http://buildcompiler.org/transform_{plasmid_displayIds[0]}_1/1" + ) + + self.assertIsInstance( + transform_activity, + sbol2.Activity, + ) + + self.assertEqual( + len(transform_activity.usages), + 2, + ) + + self.assertEqual( + len(transform_activity.associations), + 1, + ) + + association = transform_activity.associations[0] + + self.assertIsNotNone(association.plan) + + self.assertIsNotNone(association.agent) + + # Verify transformed strain + transformed_strain = transformation_doc.get( + f"http://buildcompiler.org/E_coli_DH5alpha_with_{plasmid_displayIds[0]}/1" + ) + + self.assertIsInstance( + transformed_strain, + sbol2.ModuleDefinition, + ) + + self.assertEqual( + len(transformed_strain.modules), + 1, + ) + + self.assertEqual( + len(transformed_strain.functionalComponents), + 1, + ) + + # Verify transformed implementation + transformed_impl = transformation_doc.get( + f"http://buildcompiler.org/E_coli_DH5alpha_with_{plasmid_displayIds[0]}_impl/1" + ) + + self.assertIsInstance( + transformed_impl, + sbol2.Implementation, + ) + + self.assertEqual( + transformed_impl.built, + transformed_strain.identity, + ) + + self.assertEqual( + transformed_impl.wasGeneratedBy, + [transform_activity.identity], + ) + + # Verify protocol artifacts/logging + protocol_artifacts = result["protocol_artifacts"] + + self.assertIn( + "ot2_script", + protocol_artifacts, + ) + + self.assertIn( + "human_instructions", + protocol_artifacts, + ) + + self.assertIn( + "logs", + protocol_artifacts, + ) + + self.assertEqual( + len(protocol_artifacts["logs"]), + 2, + ) + + # Error handling + invalid_plasmid = copy.deepcopy(self.buildcompiler.indexed_plasmids[4]) + + invalid_plasmid.plasmid_implementations = [] + + with self.assertRaises( + ValueError, msg="Plasmid object with no implementations should throw error" + ): + self.buildcompiler.transformation( + assembly_products=[invalid_plasmid], + transformation_doc=transformation_doc, + ) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_files/DVK_AE.xml b/tests/test_files/DVK_AE.xml new file mode 100644 index 0000000..d8ab2b6 --- /dev/null +++ b/tests/test_files/DVK_AE.xml @@ -0,0 +1,443 @@ + + + + + DVK_AE + 1 + DVK_AE + 2026-02-05T23:49:40 + + + + + + + + + + + Fusion_Site_E_4 + 1 + + + + + + + + + + + Fusion_Site_A_2 + 1 + + + + + + + + + + + dvk_backbone_core_5 + 1 + + + + + + + + + + + LacZ_cassette_3 + 1 + + + + + + + + + + + DVK_AEAnnotation0 + 1 + + + + + + + location0 + 1 + + + + 1 + 4 + + + + + + + + + + DVK_AEAnnotation1 + 1 + + + + + + + location1 + 1 + + + + 5 + 500 + + + + + + + + + + DVK_AEAnnotation2 + 1 + + + + + + + location2 + 1 + + + + 501 + 504 + + + + + + + + + + DVK_AEAnnotation3 + 1 + + + + + + + location3 + 1 + + + + 505 + 2731 + + + + + + + + + + DVK_AEConstraint1 + 1 + + + + + + + + + + + + DVK_AEConstraint2 + 1 + + + + + + + + + + + + DVK_AEConstraint3 + 1 + + + + + + + + + + + + + Fusion_Site_A + 1 + Fusion_Site_A + MoClo standard fusion site A + + + + + 26479688 + + + + + + + Fusion_Site_E + 1 + Fusion_Site_E + MoClo standard fusion site E + + + + + 26479688 + + + + + + + LacZ_cassette + 1 + LacZ_cassette + MoClo IPTG inducible LacZ alpha subunit + 2026-04-30T18:50:31 + + + + + 26479688 + + + + + + + dvk_backbone_core + 1 + This is the backbone core for Destination Vector Kanamycin (DVK) plasmids. Based on the pSB1K3 plasmid and contains kanamycin gene and a high copy number origin of replication. + + + + 26479688 + + + + + + Component_ori + 1 + + + + 26479688 + + + + + + + + Component_kan + 1 + + + + 26479688 + + + + + + + + origin_of_replication_pSB1A2_annotation + 1 + + + + 26479688 + + + + ori + 1 + + + + 26479688 + 282 + 870 + + + + + + + + + + kan_region_annotation + 1 + + + + 26479688 + + + + kan + 1 + + + + 26479688 + 1103 + 1929 + + + + + + + + + + + kan_region + 1 + Kanamycin resistance gene from the pSB1K3 plasmid. + 2026-04-16T22:44:18 + + + + 26479688 + + + + + + + + origin_of_replication_pSB1A2 + 1 + Origin of replication from the pSB1A2/pSB1K3 plasmid. pSB1A2 is a high copy number plasmid. The replication origin is a pUC19-derived pMB1 (copy number of 100-300 per cell + 2026-04-16T22:42:31 + + + + 26479688 + + + + + + + DVK_AE_sequence + 1 + + + + GGAGAGAGACCTGCACCATATGCGGTGTGAAATACCGCACAGATGCGTAAGGAGAAAATACCGCATCAGGCGCCATTCGCCATTCAGGCTGCGCAACTGTTGGGAAGGGCGATCGGTGCGGGCCTCTTCGCTATTACGCCAGCTGGCGAAAGGGGGATGTGCTGCAAGGCGATTAAGTTGGGTAACGCCAGGGTTTTCCCAGTCACGACGTTGTAAAACGACGGCCAGTGAATTCGAGCTCGGTACCCGGGGATCCTCTAGAGTCGACCTGCAGGCATGCAAGCTTGGCGTAATCATGGTCATAGCTGTTTCCTGTGTGAAATTGTTATCCGCTCACAATTCCACACAACATACGAGCCGGAAGCATAAAGTGTAAAGCCTGGGGTGCCTAATGAGTGAGCTAACTCACATTAATTGCGTTGCGCTCACTGCCCGCTTTCCAGTCGGGAAACCTGTCGTGCCAGCTGCATTAATGAATCGGCCAACGCGCGGGGGTCTCTGCTTatgtcttctactagtagcggccgctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtggaagacat + + + + + LacZ_cassette_sequence + 1 + LacZ_cassette Sequence + + + + AGAGACCTGCACCATATGCGGTGTGAAATACCGCACAGATGCGTAAGGAGAAAATACCGCATCAGGCGCCATTCGCCATTCAGGCTGCGCAACTGTTGGGAAGGGCGATCGGTGCGGGCCTCTTCGCTATTACGCCAGCTGGCGAAAGGGGGATGTGCTGCAAGGCGATTAAGTTGGGTAACGCCAGGGTTTTCCCAGTCACGACGTTGTAAAACGACGGCCAGTGAATTCGAGCTCGGTACCCGGGGATCCTCTAGAGTCGACCTGCAGGCATGCAAGCTTGGCGTAATCATGGTCATAGCTGTTTCCTGTGTGAAATTGTTATCCGCTCACAATTCCACACAACATACGAGCCGGAAGCATAAAGTGTAAAGCCTGGGGTGCCTAATGAGTGAGCTAACTCACATTAATTGCGTTGCGCTCACTGCCCGCTTTCCAGTCGGGAAACCTGTCGTGCCAGCTGCATTAATGAATCGGCCAACGCGCGGGGGTCTCT + + + + + Fusion_Site_A_sequence + 1 + Fusion_Site_A Sequence + + + + GGAG + + + + + Fusion_Site_E_sequence + 1 + Fusion_Site_E Sequence + + + + GCTT + + + + + dvk_backbone_core_seq + 1 + + + + 26479688 + atgtcttctactagtagcggccgctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtggaagacat + + + + + kan_region_seq + 1 + + + + 26479688 + caaggggtgttatgagccatattcaacgggaaacgtcttgctccaggccgcgattaaattccaacatggatgctgatttatatgggtataaatgggctcgcgataatgtcgggcaatcaggtgcgacaatctatcgattgtatgggaagcccgatgcgccagagttgtttctgaaacatggcaaaggtagcgttgccaatgatgttacagatgagatggtcagactaaactggctgacggaatttatgcctcttccgaccatcaagcattttatccgtactcctgatgatgcatggttactcaccactgcgatccccgggaaaacagcattccaggtattagaagaatatcctgattcaggtgaaaatattgttgatgcgctggcagtgttcctgcgccggttgcattcgattcctgtttgtaattgtccttttaacagcgatcgcgtatttcgtctcgctcaggcgcaatcacgaatgaataacggtttggttgatgcgagtgattttgatgacgagcgtaatggctggcctgttgaacaagtctggaaagaaatgcataagcttttgccattctcaccggattcagtcgtcactcatggtgatttctcacttgataaccttatttttgacgaggggaaattaataggttgtattgatgttggacgagtcggaatcgcagaccgataccaggatcttgccatcctatggaactgcctcggtgagttttctccttcattacagaaacggctttttcaaaaatatggtattgataatcctgatatgaataaattgcagtttcatttgatgctcgatgagtttttctaa + + + + + origin_of_replication_seq + 1 + + + + 26479688 + ttgagatcctttttttctgcgcgtaatctgctgcttgcaaacaaaaaaaccaccgctaccagcggtggtttgtttgccggatcaagagctaccaactctttttccgaaggtaactggcttcagcagagcgcagataccaaatactgttcttctagtgtagccgtagttaggccaccacttcaagaactctgtagcaccgcctacatacctcgctctgctaatcctgttaccagtggctgctgccagtggcgataagtcgtgtcttaccgggttggactcaagacgatagttaccggataaggcgcagcggtcgggctgaacggggggttcgtgcacacagcccagcttggagcgaacgacctacaccgaactgagatacctacagcgtgagctatgagaaagcgccacgcttcccgaagggagaaaggcggacaggtatccggtaagcggcagggtcggaacaggagagcgcacgagggagcttccagggggaaacgcctggtatctttatagtcctgtcgggtttcgccacctctgacttgagcgtcgatttttgtgatgctcgtcaggggggcggagcctgtggaaa + + + \ No newline at end of file diff --git a/tests/test_files/ExampleLvl2_design.xml b/tests/test_files/ExampleLvl2_design.xml new file mode 100644 index 0000000..968ccd3 --- /dev/null +++ b/tests/test_files/ExampleLvl2_design.xml @@ -0,0 +1,593 @@ + + + + + Two_genes + 1 + + + + + + + + Gen1_Component + 1 + + + + + + + + Gen_Component + 1 + + + + + + + + Two_genes_SequenceAnnotation + 1 + + + + Two_genes_SequenceAnnotation_Range + 1 + 1 + 906 + + + + + + + + + + Two_genes_SequenceAnnotation1 + 1 + + + + Two_genes_SequenceAnnotation1_Range + 1 + 907 + 1771 + + + + + + + + + + Two_genes_SequenceConstraint + 1 + + + + + + + + + + Gen1 + 1 + + + + + + + RBS1_Component + 1 + + + + + + + + Pro1_Component + 1 + + + + + + + + Ter1_Component + 1 + + + + + + + + CDS1_Component + 1 + + + + + + + + Gen1_SequenceAnnotation2 + 1 + + + + Gen1_SequenceAnnotation2_Range + 1 + 56 + 736 + + + + + + + + + + Gen1_SequenceAnnotation3 + 1 + + + + Gen1_SequenceAnnotation3_Range + 1 + 737 + 865 + + + + + + + + + + Gen1_SequenceAnnotation1 + 1 + + + + Gen1_SequenceAnnotation1_Range + 1 + 36 + 55 + + + + + + + + + + Gen1_SequenceAnnotation + 1 + + + + Gen1_SequenceAnnotation_Range + 1 + 1 + 35 + + + + + + + + + + Gen1_SequenceConstraint1 + 1 + + + + + + + + + Gen1_SequenceConstraint2 + 1 + + + + + + + + + Gen1_SequenceConstraint + 1 + + + + + + + + + + B0015 + 1 + B0015 + MoClo Basic Part: Double terminator (B0010:B0012) + + + + + 26479688 + + + + + + + B0032 + 1 + B0032 + MoClo Basic Part: RBS - Weiss RBS, medium strength. Modified from Bba_B0032 to adjust spacing in MC system. + + + + + 26479688 + + + + + + + J23116 + 1 + J23116 + MoClo Basic Part: Constitutive promoter - Anderson series - low strength + + + + + 26479688 + + + + + + + E1010m_rfp + 1 + E1010m_rfp + MoClo Basic Part: CDS - Fluorescent protein. Red. Modified from Bba_E1010 to fix illegal sites. + + + + + 26479688 + + + + + + + B0033 + 1 + B0033 + MoClo Basic Part: RBS - Weiss RBS, low strength. Modified from Bba_B0033 to adjust spacing in MC system. + + + + + 26479688 + + + + + + + E0040m_gfp + 1 + E0040m_gfp + MoClo Basic Part: CDS - Fluorescent protein. Green. Modified from Bba_E0040 to fix illegal site. + + + + + 26479688 + + + + + + + Gen + 1 + + + + + + + CDS_Component + 1 + + + + + + + + Pro_Component + 1 + + + + + + + + RBS_Component + 1 + + + + + + + + Ter_Component + 1 + + + + + + + + Gen_SequenceAnnotation2 + 1 + + + + Gen_SequenceAnnotation2_Range + 1 + 58 + 777 + + + + + + + + + + Gen_SequenceAnnotation + 1 + + + + Gen_SequenceAnnotation_Range + 1 + 1 + 35 + + + + + + + + + + Gen_SequenceAnnotation3 + 1 + + + + Gen_SequenceAnnotation3_Range + 1 + 778 + 906 + + + + + + + + + + Gen_SequenceAnnotation1 + 1 + + + + Gen_SequenceAnnotation1_Range + 1 + 36 + 57 + + + + + + + + + + Gen_SequenceConstraint2 + 1 + + + + + + + + + Gen_SequenceConstraint1 + 1 + + + + + + + + + Gen_SequenceConstraint + 1 + + + + + + + + + + J23100 + 1 + J23100 + MoClo Basic Part: Constitutive promoter - Anderson series - high strength + + + + + 26479688 + + + + + + + Two_genesSequence + 1 + + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCAGAGTCACACAGGAAAGTACTAATGCGTAAAGGAGAAGAACTTTTCACTGGAGTTGTCCCAATTCTTGTTGAATTAGATGGTGATGTTAATGGGCACAAATTTTCTGTCAGTGGAGAGGGTGAAGGTGATGCAACATACGGAAAACTTACCCTTAAATTTATTTGCACTACTGGAAAACTACCTGTTCCATGGCCAACACTTGTCACTACTTTCGGTTATGGTGTTCAATGCTTTGCGAGATACCCAGATCATATGAAACAGCATGACTTTTTCAAGAGTGCCATGCCCGAAGGTTATGTACAGGAAAGAACTATATTTTTCAAAGATGACGGGAACTACAAGACACGTGCTGAAGTCAAGTTTGAAGGTGATACCCTTGTTAATAGAATCGAGTTAAAAGGTATTGATTTTAAAGAAGATGGAAACATTCTTGGACACAAATTGGAATACAACTATAACTCACACAATGTATACATCATGGCAGACAAACAAAAGAATGGAATCAAAGTTAACTTCAAAATTAGACACAACATTGAAGATGGAAGCGTTCAACTAGCAGACCATTATCAACAAAATACTCCAATTGGCGATGGCCCTGTCCTTTTACCAGACAACCATTACCTGTCCACACAATCTGCCCTTTCGAAAGATCCCAACGAAAAGAGAGATCACATGGTCCTTCTTGAGTTTGTAACAGCTGCTGGGATTACACATGGCATGGATGAACTATACAAATAATAACCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATATTGACAGCTAGCTCAGTCCTAGGGACTATGCTAGCAGAGTCACACAGGACTACTAATGGCTTCCTCCGAGGATGTTATCAAAGAGTTCATGCGTTTCAAAGTTCGTATGGAAGGTTCCGTTAACGGTCACGAGTTCGAAATCGAAGGTGAAGGTGAAGGTCGTCCGTACGAAGGTACCCAGACCGCTAAACTGAAAGTTACCAAAGGTGGTCCGCTGCCGTTCGCTTGGGACATCCTGTCCCCGCAGTTCCAGTACGGTTCCAAAGCTTACGTTAAACACCCGGCTGACATCCCGGACTACCTGAAACTGTCCTTCCCGGAAGGTTTCAAATGGGAACGTGTTATGAACTTCGAAGATGGTGGTGTTGTTACCGTTACCCAGGACTCCTCCCTGCAAGACGGTGAGTTCATCTACAAAGTTAAACTGCGTGGTACCAACTTCCCGTCCGACGGTCCGGTTATGCAGAAAAAAACCATGGGTTGGGAAGCTTCCACCGAACGTATGTACCCGGAGGATGGTGCTCTGAAAGGTGAAATCAAAATGCGTCTGAAACTGAAAGACGGTGGTCACTACGACGCTGAAGTTAAAACCACCTACATGGCTAAAAAACCGGTTCAGCTGCCGGGTGCTTACAAAACCGACATCAAACTGGACATCACCTCCCACAACGAGGACTACACCATCGTTGAACAGTACGAACGTGCTGAAGGTCGTCACTCCACCGGTGCTTAATAACCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + + + Gen1Sequence + 1 + + TTGACAGCTAGCTCAGTCCTAGGGACTATGCTAGCAGAGTCACACAGGACTACTAATGGCTTCCTCCGAGGATGTTATCAAAGAGTTCATGCGTTTCAAAGTTCGTATGGAAGGTTCCGTTAACGGTCACGAGTTCGAAATCGAAGGTGAAGGTGAAGGTCGTCCGTACGAAGGTACCCAGACCGCTAAACTGAAAGTTACCAAAGGTGGTCCGCTGCCGTTCGCTTGGGACATCCTGTCCCCGCAGTTCCAGTACGGTTCCAAAGCTTACGTTAAACACCCGGCTGACATCCCGGACTACCTGAAACTGTCCTTCCCGGAAGGTTTCAAATGGGAACGTGTTATGAACTTCGAAGATGGTGGTGTTGTTACCGTTACCCAGGACTCCTCCCTGCAAGACGGTGAGTTCATCTACAAAGTTAAACTGCGTGGTACCAACTTCCCGTCCGACGGTCCGGTTATGCAGAAAAAAACCATGGGTTGGGAAGCTTCCACCGAACGTATGTACCCGGAGGATGGTGCTCTGAAAGGTGAAATCAAAATGCGTCTGAAACTGAAAGACGGTGGTCACTACGACGCTGAAGTTAAAACCACCTACATGGCTAAAAAACCGGTTCAGCTGCCGGGTGCTTACAAAACCGACATCAAACTGGACATCACCTCCCACAACGAGGACTACACCATCGTTGAACAGTACGAACGTGCTGAAGGTCGTCACTCCACCGGTGCTTAATAACCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + + + GenSequence + 1 + + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCAGAGTCACACAGGAAAGTACTAATGCGTAAAGGAGAAGAACTTTTCACTGGAGTTGTCCCAATTCTTGTTGAATTAGATGGTGATGTTAATGGGCACAAATTTTCTGTCAGTGGAGAGGGTGAAGGTGATGCAACATACGGAAAACTTACCCTTAAATTTATTTGCACTACTGGAAAACTACCTGTTCCATGGCCAACACTTGTCACTACTTTCGGTTATGGTGTTCAATGCTTTGCGAGATACCCAGATCATATGAAACAGCATGACTTTTTCAAGAGTGCCATGCCCGAAGGTTATGTACAGGAAAGAACTATATTTTTCAAAGATGACGGGAACTACAAGACACGTGCTGAAGTCAAGTTTGAAGGTGATACCCTTGTTAATAGAATCGAGTTAAAAGGTATTGATTTTAAAGAAGATGGAAACATTCTTGGACACAAATTGGAATACAACTATAACTCACACAATGTATACATCATGGCAGACAAACAAAAGAATGGAATCAAAGTTAACTTCAAAATTAGACACAACATTGAAGATGGAAGCGTTCAACTAGCAGACCATTATCAACAAAATACTCCAATTGGCGATGGCCCTGTCCTTTTACCAGACAACCATTACCTGTCCACACAATCTGCCCTTTCGAAAGATCCCAACGAAAAGAGAGATCACATGGTCCTTCTTGAGTTTGTAACAGCTGCTGGGATTACACATGGCATGGATGAACTATACAAATAATAACCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + + + B0015_sequence + 1 + B0015 Sequence + + + + CCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + + + B0032_sequence + 1 + B0032 Sequence + + + + AGAGTCACACAGGAAAGTACTA + + + + + J23100_sequence + 1 + J23100 Sequence + + + + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGC + + + + + J23116_sequence + 1 + J23116 Sequence + + + + TTGACAGCTAGCTCAGTCCTAGGGACTATGCTAGC + + + + + E0040m_gfp_sequence + 1 + E0040m_gfp Sequence + + + + ATGCGTAAAGGAGAAGAACTTTTCACTGGAGTTGTCCCAATTCTTGTTGAATTAGATGGTGATGTTAATGGGCACAAATTTTCTGTCAGTGGAGAGGGTGAAGGTGATGCAACATACGGAAAACTTACCCTTAAATTTATTTGCACTACTGGAAAACTACCTGTTCCATGGCCAACACTTGTCACTACTTTCGGTTATGGTGTTCAATGCTTTGCGAGATACCCAGATCATATGAAACAGCATGACTTTTTCAAGAGTGCCATGCCCGAAGGTTATGTACAGGAAAGAACTATATTTTTCAAAGATGACGGGAACTACAAGACACGTGCTGAAGTCAAGTTTGAAGGTGATACCCTTGTTAATAGAATCGAGTTAAAAGGTATTGATTTTAAAGAAGATGGAAACATTCTTGGACACAAATTGGAATACAACTATAACTCACACAATGTATACATCATGGCAGACAAACAAAAGAATGGAATCAAAGTTAACTTCAAAATTAGACACAACATTGAAGATGGAAGCGTTCAACTAGCAGACCATTATCAACAAAATACTCCAATTGGCGATGGCCCTGTCCTTTTACCAGACAACCATTACCTGTCCACACAATCTGCCCTTTCGAAAGATCCCAACGAAAAGAGAGATCACATGGTCCTTCTTGAGTTTGTAACAGCTGCTGGGATTACACATGGCATGGATGAACTATACAAATAATAA + + + + + E1010m_rfp_sequence + 1 + E1010m_rfp Sequence + + + + ATGGCTTCCTCCGAGGATGTTATCAAAGAGTTCATGCGTTTCAAAGTTCGTATGGAAGGTTCCGTTAACGGTCACGAGTTCGAAATCGAAGGTGAAGGTGAAGGTCGTCCGTACGAAGGTACCCAGACCGCTAAACTGAAAGTTACCAAAGGTGGTCCGCTGCCGTTCGCTTGGGACATCCTGTCCCCGCAGTTCCAGTACGGTTCCAAAGCTTACGTTAAACACCCGGCTGACATCCCGGACTACCTGAAACTGTCCTTCCCGGAAGGTTTCAAATGGGAACGTGTTATGAACTTCGAAGATGGTGGTGTTGTTACCGTTACCCAGGACTCCTCCCTGCAAGACGGTGAGTTCATCTACAAAGTTAAACTGCGTGGTACCAACTTCCCGTCCGACGGTCCGGTTATGCAGAAAAAAACCATGGGTTGGGAAGCTTCCACCGAACGTATGTACCCGGAGGATGGTGCTCTGAAAGGTGAAATCAAAATGCGTCTGAAACTGAAAGACGGTGGTCACTACGACGCTGAAGTTAAAACCACCTACATGGCTAAAAAACCGGTTCAGCTGCCGGGTGCTTACAAAACCGACATCAAACTGGACATCACCTCCCACAACGAGGACTACACCATCGTTGAACAGTACGAACGTGCTGAAGGTCGTCACTCCACCGGTGCTTAATAA + + + + + B0033_sequence + 1 + B0033 Sequence + + + + AGAGTCACACAGGACTACTA + + + + + Two_genes_SBOLDesignerActivity + 1 + Gonzalo Vidal + 2026-03-24T16:18:54.110-06:00 + + + + Association + 1 + + + + + + diff --git a/tests/test_files/comp_tu.xml b/tests/test_files/comp_tu.xml new file mode 100644 index 0000000..8a5a343 --- /dev/null +++ b/tests/test_files/comp_tu.xml @@ -0,0 +1,343 @@ + + + + + B0015 + 1 + B0015 + MoClo Basic Part: Double terminator (B0010:B0012) + + + + + 26479688 + + + + + + + i2mLg6N9A + 1 + + + + + + + B0015_4 + 1 + + + + + + + + C0062_luxR_3 + 1 + + + + + + + + J23100_1 + 1 + + + + + + + + B0033_2 + 1 + + + + + + + + i2mLg6N9AAnnotation1 + 1 + + + + location1 + 1 + 36 + 55 + + + + + + + + + + i2mLg6N9AAnnotation2 + 1 + + + + location2 + 1 + 56 + 811 + + + + + + + + + + i2mLg6N9AAnnotation3 + 1 + + + + location3 + 1 + 812 + 940 + + + + + + + + + + i2mLg6N9AAnnotation0 + 1 + + + + location0 + 1 + 1 + 35 + + + + + + + + + + i2mLg6N9AConstraint3 + 1 + + + + + + + + + i2mLg6N9AConstraint1 + 1 + + + + + + + + + i2mLg6N9AConstraint2 + 1 + + + + + + + + + + B0033 + 1 + B0033 + MoClo Basic Part: RBS - Weiss RBS, low strength. Modified from Bba_B0033 to adjust spacing in MC system. + + + + + 26479688 + + + + + + + J23100 + 1 + J23100 + MoClo Basic Part: Constitutive promoter - Anderson series - high strength + + + + + 26479688 + + + + + + + C0062_luxR + 1 + C0062_luxR + MoClo Basic Part: CDS - Controller protein, luxR repressor/activator (in concert with HSL, represses pLuxR(pR) R0063. Also up-regulates pLuxR(pL) R0062) + + + + + 26479688 + + + + + + + B0015_sequence + 1 + B0015 Sequence + + + + CCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + + + i2mLg6N9A_sequence + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGC + + + + + J23100_sequence + 1 + J23100 Sequence + + + + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGC + + + + + C0062_luxR_sequence + 1 + C0062_luxR Sequence + + + + ATGAAAAACATAAATGCCGACGACACATACAGAATAATTAATAAAATTAAAGCTTGTAGAAGCAATAATGATATTAATCAATGCTTATCTGATATGACTAAAATGGTACATTGTGAATATTATTTACTCGCGATCATTTATCCTCATTCTATGGTTAAATCTGATATTTCAATCCTAGATAATTACCCTAAAAAATGGAGGCAATATTATGATGACGCTAATTTAATAAAATATGATCCTATAGTAGATTATTCTAACTCCAATCATTCACCAATTAATTGGAATATATTTGAAAACAATGCTGTAAATAAAAAATCTCCAAATGTAATTAAAGAAGCGAAAACATCAGGTCTTATCACTGGGTTTAGTTTCCCTATTCATACGGCTAACAATGGCTTCGGAATGCTTAGTTTTGCACATTCAGAAAAAGACAACTATATAGATAGTTTATTTTTACATGCGTGTATGAACATACCATTAATTGTTCCTTCTCTAGTTGATAATTATCGAAAAATAAATATAGCAAATAATAAATCAAACAACGATTTAACCAAAAGAGAAAAAGAATGTTTAGCGTGGGCATGCGAAGGAAAAAGCTCTTGGGATATTTCAAAAATATTAGGTTGCAGTGAGCGTACTGTCACTTTCCATTTAACCAATGCGCAAATGAAACTCAATACAACAAACCGCTGCCAAAGTATTTCTAAAGCAATTTTAACAGGAGCAATTGATTGCCCATACTTTAAAAATTAATAA + + + + + B0033_sequence + 1 + B0033 Sequence + + + + AGAGTCACACAGGACTACTA + + + + + J23100_Layout + + + + + B0015_Layout + + + + + C0062_luxR_Layout + + + + + B0033_Layout + + + + + i2mLg6N9A_Layout + + + + 455.0 + 334.5 + 200.0 + 100.0 + container + + + + + 0.0 + 50.0 + 200.0 + 1.0 + backbone + + + + + 0.0 + 0.0 + 50.0 + 100.0 + J23100_1 + + + + + + 50.0 + 0.0 + 50.0 + 100.0 + B0033_2 + + + + + + 100.0 + 0.0 + 50.0 + 100.0 + C0062_luxR_3 + + + + + + 150.0 + 0.0 + 50.0 + 100.0 + B0015_4 + + + + + diff --git a/tests/test_files/mocloparts116.xml b/tests/test_files/mocloparts116.xml new file mode 100644 index 0000000..d683a12 --- /dev/null +++ b/tests/test_files/mocloparts116.xml @@ -0,0 +1,307 @@ + + + + + E0030_yfp + 1 + E0030_yfp + MoClo Basic Part: CDS - Fluorescent protein. Yellow. + 26479688 + + + + + + + J23116 + 1 + J23116 + MoClo Basic Part: Constitutive promoter - Anderson series - low strength + 26479688 + + + + + + + B0034 + 1 + B0034 + MoClo Basic Part: RBS - Weiss RBS, high strength. Modified from Bba_B0034 to adjust spacing in MC system. + 26479688 + + + + + + + i0mwvNcgH + 1 + + + + + + + J23116_1 + 1 + + + + + + + + B0034_2 + 1 + + + + + + + + E0030_yfp_3 + 1 + + + + + + + + B0015_4 + 1 + + + + + + + + i0mwvNcgHAnnotation3 + 1 + + + + location3 + 1 + 780 + 908 + + + + + + + + + + i0mwvNcgHAnnotation1 + 1 + + + + location1 + 1 + 36 + 56 + + + + + + + + + + i0mwvNcgHAnnotation2 + 1 + + + + location2 + 1 + 57 + 779 + + + + + + + + + + i0mwvNcgHAnnotation0 + 1 + + + + location0 + 1 + 1 + 35 + + + + + + + + + + i0mwvNcgHConstraint3 + 1 + + + + + + + + + i0mwvNcgHConstraint1 + 1 + + + + + + + + + i0mwvNcgHConstraint2 + 1 + + + + + + + + + + B0015 + 1 + B0015 + MoClo Basic Part: Double terminator (B0010:B0012) + 26479688 + + + + + + + E0030_yfp_sequence + ATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCTTCGGCTACGGCCTGCAATGCTTCGCCCGCTACCCCGACCACATGAAGCTGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCTACCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAATAA + + + + + J23116_sequence + TTGACAGCTAGCTCAGTCCTAGGGACTATGCTAGC + + + + + B0015_sequence + CCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + + + i0mwvNcgH_sequence + TTGACAGCTAGCTCAGTCCTAGGGACTATGCTAGC + + + + + B0034_sequence + AGAGAAAGAGGAGAAATACTA + + + + + J23116_Layout + + + + + B0015_Layout + + + + + i0mwvNcgH_Layout + + + + 437.5 + 337.0 + 200.0 + 100.0 + container + + + + + 0.0 + 50.0 + 200.0 + 1.0 + backbone + + + + + 0.0 + 0.0 + 50.0 + 100.0 + J23116_1 + + + + + + 50.0 + 0.0 + 50.0 + 100.0 + B0034_2 + + + + + + 100.0 + 0.0 + 50.0 + 100.0 + E0030_yfp_3 + + + + + + 150.0 + 0.0 + 50.0 + 100.0 + B0015_4 + + + + + + + E0030_yfp_Layout + + + + + B0034_Layout + + + \ No newline at end of file diff --git a/tests/test_files/pB0015_DE.xml b/tests/test_files/pB0015_DE.xml new file mode 100644 index 0000000..7379b8a --- /dev/null +++ b/tests/test_files/pB0015_DE.xml @@ -0,0 +1,440 @@ + + + + + pB0015_DE + 1 + pB0015_DE + + + + + + + + + + Fusion_Site_D_2 + 1 + + + + + + + + + + + B0015_3 + 1 + + + + + + + + + + + dva_backbone_core_5 + 1 + + + + + + + + + + + Fusion_Site_E_4 + 1 + + + + + + + + + + + pB0015_DEAnnotation0 + 1 + + + + + + + location0 + 1 + + + + 1 + 4 + + + + + + + + + + pB0015_DEAnnotation1 + 1 + + + + + + + location1 + 1 + + + + 5 + 133 + + + + + + + + + + pB0015_DEAnnotation2 + 1 + + + + + + + location2 + 1 + + + + 134 + 137 + + + + + + + + + + pB0015_DEAnnotation3 + 1 + + + + + + + location3 + 1 + + + + 138 + 2237 + + + + + + + + + + pB0015_DEConstraint1 + 1 + + + + + + + + + + + + pB0015_DEConstraint2 + 1 + + + + + + + + + + + + pB0015_DEConstraint3 + 1 + + + + + + + + + + + + + B0015 + 1 + B0015 + MoClo Basic Part: Double terminator (B0010:B0012) + + + + + 26479688 + + + + + + + Fusion_Site_D + 1 + Fusion_Site_D + MoClo standard fusion site D + + + + + 26479688 + + + + + + + Fusion_Site_E + 1 + Fusion_Site_E + MoClo standard fusion site E + + + + + 26479688 + + + + + + + dva_backbone_core + 1 + This is the backbone core for Destination Vector Ampicillin (DVA) plasmids. Based on the pSB1A2 plasmid and contains an ampicillin resistance gene and a high copy number origin of replication. + + + + 26479688 + + + + + + Component_ori + 1 + + + + 26479688 + + + + + + + + Component_amp + 1 + + + + 26479688 + + + + + + + + origin_of_replication_pSB1A2_annotation + 1 + + + + 26479688 + + + + ori + 1 + + + + 26479688 + 272 + 860 + + + + + + + + + + amp_region_annotation + 1 + + + + 26479688 + + + + amp + 1 + + + + 26479688 + 1031 + 1996 + + + + + + + + + + + amp_region + 1 + Ampicillin resistance gene from the pSB1A2 plasmid. + 2026-02-05T23:21:12 + + + + 26479688 + + + + + + + + origin_of_replication_pSB1A2 + 1 + Origin of replication from the pSB1A2/pSB1K3 plasmid. pSB1A2 is a high copy number plasmid. The replication origin is a pUC19-derived pMB1 (copy number of 100-300 per cell + 2026-04-16T22:42:31 + + + + 26479688 + + + + + + + pB0015_DE_sequence + 1 + + + + AGGTCCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATAGCTTagagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca + + + + + B0015_sequence + 1 + B0015 Sequence + + + + CCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + + + Fusion_Site_D_sequence + 1 + Fusion_Site_D Sequence + + + + AGGT + + + + + Fusion_Site_E_sequence + 1 + Fusion_Site_E Sequence + + + + GCTT + + + + + dva_backbone_core_seq + 1 + + + + 26479688 + agagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca + + + + + amp_region_seq + 1 + + + + 26479688 + cgcggaacccctatttgtttatttttctaaatacattcaaatatgtatccgctcatgagacaataaccctgataaatgcttcaataatattgaaaaaggaagagtatgagtattcaacatttccgtgtcgcccttattcccttttttgcggcattttgccttcctgtttttgctcacccagaaacgctggtgaaagtaaaagatgctgaagatcagttgggtgcacgagtgggttacatcgaactggatctcaacagcggtaagatccttgagagttttcgccccgaagaacgttttccaatgatgagcacttttaaagttctgctatgtggcgcggtattatcccgtattgacgccgggcaagagcaactcggtcgccgcatacactattctcagaatgacttggttgagtactcaccagtcacagaaaagcatcttacggatggcatgacagtaagagaattatgcagtgctgccataaccatgagtgataacactgcggccaacttacttctgacaacgatcggaggaccgaaggagctaaccgcttttttgcacaacatgggggatcatgtaactcgccttgatcgttgggaaccggagctgaatgaagccataccaaacgacgagcgtgacaccacgatgcctgtagcaatggcaacaacgttgcgcaaactattaactggcgaactacttactctagcttcccggcaacaattaatagactggatggaggcggataaagttgcaggaccacttctgcgctcggcccttccggctggctggtttattgctgataaatctggagccggtgagcgtgggtcgcgcggtatcattgcagcactggggccagatggtaagccctcccgtatcgtagttatctacacgacggggagtcaggcaactatggatgaacgaaatagacagatcgctgagataggtgcctcactgattaagcattggtaa + + + + + origin_of_replication_seq + 1 + + + + 26479688 + ttgagatcctttttttctgcgcgtaatctgctgcttgcaaacaaaaaaaccaccgctaccagcggtggtttgtttgccggatcaagagctaccaactctttttccgaaggtaactggcttcagcagagcgcagataccaaatactgttcttctagtgtagccgtagttaggccaccacttcaagaactctgtagcaccgcctacatacctcgctctgctaatcctgttaccagtggctgctgccagtggcgataagtcgtgtcttaccgggttggactcaagacgatagttaccggataaggcgcagcggtcgggctgaacggggggttcgtgcacacagcccagcttggagcgaacgacctacaccgaactgagatacctacagcgtgagctatgagaaagcgccacgcttcccgaagggagaaaggcggacaggtatccggtaagcggcagggtcggaacaggagagcgcacgagggagcttccagggggaaacgcctggtatctttatagtcctgtcgggtttcgccacctctgacttgagcgtcgatttttgtgatgctcgtcaggggggcggagcctgtggaaa + + + \ No newline at end of file diff --git a/tests/test_files/pB0032_BC.xml b/tests/test_files/pB0032_BC.xml new file mode 100644 index 0000000..53770bd --- /dev/null +++ b/tests/test_files/pB0032_BC.xml @@ -0,0 +1,440 @@ + + + + + pB0032_BC + 1 + pB0032_BC + + + + + + + + + + Fusion_Site_C_4 + 1 + + + + + + + + + + + Fusion_Site_B_2 + 1 + + + + + + + + + + + dva_backbone_core_5 + 1 + + + + + + + + + + + B0032_3 + 1 + + + + + + + + + + + pB0032_BCAnnotation0 + 1 + + + + + + + location0 + 1 + + + + 1 + 4 + + + + + + + + + + pB0032_BCAnnotation1 + 1 + + + + + + + location1 + 1 + + + + 5 + 26 + + + + + + + + + + pB0032_BCAnnotation2 + 1 + + + + + + + location2 + 1 + + + + 27 + 30 + + + + + + + + + + pB0032_BCAnnotation3 + 1 + + + + + + + location3 + 1 + + + + 31 + 2130 + + + + + + + + + + pB0032_BCConstraint1 + 1 + + + + + + + + + + + + pB0032_BCConstraint2 + 1 + + + + + + + + + + + + pB0032_BCConstraint3 + 1 + + + + + + + + + + + + + B0032 + 1 + B0032 + MoClo Basic Part: RBS - Weiss RBS, medium strength. Modified from Bba_B0032 to adjust spacing in MC system. + + + + + 26479688 + + + + + + + Fusion_Site_B + 1 + Fusion_Site_B + MoClo standard fusion site B + + + + + 26479688 + + + + + + + Fusion_Site_C + 1 + Fusion_Site_C + MoClo standard fusion site C + + + + + 26479688 + + + + + + + dva_backbone_core + 1 + This is the backbone core for Destination Vector Ampicillin (DVA) plasmids. Based on the pSB1A2 plasmid and contains an ampicillin resistance gene and a high copy number origin of replication. + + + + 26479688 + + + + + + Component_ori + 1 + + + + 26479688 + + + + + + + + Component_amp + 1 + + + + 26479688 + + + + + + + + origin_of_replication_pSB1A2_annotation + 1 + + + + 26479688 + + + + ori + 1 + + + + 26479688 + 272 + 860 + + + + + + + + + + amp_region_annotation + 1 + + + + 26479688 + + + + amp + 1 + + + + 26479688 + 1031 + 1996 + + + + + + + + + + + amp_region + 1 + Ampicillin resistance gene from the pSB1A2 plasmid. + 2026-02-05T23:21:12 + + + + 26479688 + + + + + + + + origin_of_replication_pSB1A2 + 1 + Origin of replication from the pSB1A2/pSB1K3 plasmid. pSB1A2 is a high copy number plasmid. The replication origin is a pUC19-derived pMB1 (copy number of 100-300 per cell + 2026-04-16T22:42:31 + + + + 26479688 + + + + + + + pB0032_BC_sequence + 1 + + + + TACTAGAGTCACACAGGAAAGTACTAAATGagagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca + + + + + B0032_sequence + 1 + B0032 Sequence + + + + AGAGTCACACAGGAAAGTACTA + + + + + Fusion_Site_B_sequence + 1 + Fusion_Site_B Sequence + + + + TACT + + + + + Fusion_Site_C_sequence + 1 + Fusion_Site_C Sequence + + + + AATG + + + + + dva_backbone_core_seq + 1 + + + + 26479688 + agagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca + + + + + amp_region_seq + 1 + + + + 26479688 + cgcggaacccctatttgtttatttttctaaatacattcaaatatgtatccgctcatgagacaataaccctgataaatgcttcaataatattgaaaaaggaagagtatgagtattcaacatttccgtgtcgcccttattcccttttttgcggcattttgccttcctgtttttgctcacccagaaacgctggtgaaagtaaaagatgctgaagatcagttgggtgcacgagtgggttacatcgaactggatctcaacagcggtaagatccttgagagttttcgccccgaagaacgttttccaatgatgagcacttttaaagttctgctatgtggcgcggtattatcccgtattgacgccgggcaagagcaactcggtcgccgcatacactattctcagaatgacttggttgagtactcaccagtcacagaaaagcatcttacggatggcatgacagtaagagaattatgcagtgctgccataaccatgagtgataacactgcggccaacttacttctgacaacgatcggaggaccgaaggagctaaccgcttttttgcacaacatgggggatcatgtaactcgccttgatcgttgggaaccggagctgaatgaagccataccaaacgacgagcgtgacaccacgatgcctgtagcaatggcaacaacgttgcgcaaactattaactggcgaactacttactctagcttcccggcaacaattaatagactggatggaggcggataaagttgcaggaccacttctgcgctcggcccttccggctggctggtttattgctgataaatctggagccggtgagcgtgggtcgcgcggtatcattgcagcactggggccagatggtaagccctcccgtatcgtagttatctacacgacggggagtcaggcaactatggatgaacgaaatagacagatcgctgagataggtgcctcactgattaagcattggtaa + + + + + origin_of_replication_seq + 1 + + + + 26479688 + ttgagatcctttttttctgcgcgtaatctgctgcttgcaaacaaaaaaaccaccgctaccagcggtggtttgtttgccggatcaagagctaccaactctttttccgaaggtaactggcttcagcagagcgcagataccaaatactgttcttctagtgtagccgtagttaggccaccacttcaagaactctgtagcaccgcctacatacctcgctctgctaatcctgttaccagtggctgctgccagtggcgataagtcgtgtcttaccgggttggactcaagacgatagttaccggataaggcgcagcggtcgggctgaacggggggttcgtgcacacagcccagcttggagcgaacgacctacaccgaactgagatacctacagcgtgagctatgagaaagcgccacgcttcccgaagggagaaaggcggacaggtatccggtaagcggcagggtcggaacaggagagcgcacgagggagcttccagggggaaacgcctggtatctttatagtcctgtcgggtttcgccacctctgacttgagcgtcgatttttgtgatgctcgtcaggggggcggagcctgtggaaa + + + \ No newline at end of file diff --git a/tests/test_files/pE0040_CD.xml b/tests/test_files/pE0040_CD.xml new file mode 100644 index 0000000..af9be9e --- /dev/null +++ b/tests/test_files/pE0040_CD.xml @@ -0,0 +1,440 @@ + + + + + pE0040_CD + 1 + pE0040_CD + + + + + + + + + + dva_backbone_core_5 + 1 + + + + + + + + + + + Fusion_Site_D_4 + 1 + + + + + + + + + + + Fusion_Site_C_2 + 1 + + + + + + + + + + + E0040m_gfp_3 + 1 + + + + + + + + + + + pE0040_CDAnnotation0 + 1 + + + + + + + location0 + 1 + + + + 1 + 4 + + + + + + + + + + pE0040_CDAnnotation1 + 1 + + + + + + + location1 + 1 + + + + 5 + 724 + + + + + + + + + + pE0040_CDAnnotation2 + 1 + + + + + + + location2 + 1 + + + + 725 + 728 + + + + + + + + + + pE0040_CDAnnotation3 + 1 + + + + + + + location3 + 1 + + + + 729 + 2828 + + + + + + + + + + pE0040_CDConstraint3 + 1 + + + + + + + + + + + + pE0040_CDConstraint1 + 1 + + + + + + + + + + + + pE0040_CDConstraint2 + 1 + + + + + + + + + + + + + E0040m_gfp + 1 + E0040m_gfp + MoClo Basic Part: CDS - Fluorescent protein. Green. Modified from Bba_E0040 to fix illegal site. + + + + + 26479688 + + + + + + + Fusion_Site_C + 1 + Fusion_Site_C + MoClo standard fusion site C + + + + + 26479688 + + + + + + + Fusion_Site_D + 1 + Fusion_Site_D + MoClo standard fusion site D + + + + + 26479688 + + + + + + + dva_backbone_core + 1 + This is the backbone core for Destination Vector Ampicillin (DVA) plasmids. Based on the pSB1A2 plasmid and contains an ampicillin resistance gene and a high copy number origin of replication. + + + + 26479688 + + + + + + Component_ori + 1 + + + + 26479688 + + + + + + + + Component_amp + 1 + + + + 26479688 + + + + + + + + origin_of_replication_pSB1A2_annotation + 1 + + + + 26479688 + + + + ori + 1 + + + + 26479688 + 272 + 860 + + + + + + + + + + amp_region_annotation + 1 + + + + 26479688 + + + + amp + 1 + + + + 26479688 + 1031 + 1996 + + + + + + + + + + + amp_region + 1 + Ampicillin resistance gene from the pSB1A2 plasmid. + 2026-02-05T23:21:12 + + + + 26479688 + + + + + + + + origin_of_replication_pSB1A2 + 1 + Origin of replication from the pSB1A2/pSB1K3 plasmid. pSB1A2 is a high copy number plasmid. The replication origin is a pUC19-derived pMB1 (copy number of 100-300 per cell + 2026-04-16T22:42:31 + + + + 26479688 + + + + + + + pE0040_CD_sequence + 1 + + + + AATGATGCGTAAAGGAGAAGAACTTTTCACTGGAGTTGTCCCAATTCTTGTTGAATTAGATGGTGATGTTAATGGGCACAAATTTTCTGTCAGTGGAGAGGGTGAAGGTGATGCAACATACGGAAAACTTACCCTTAAATTTATTTGCACTACTGGAAAACTACCTGTTCCATGGCCAACACTTGTCACTACTTTCGGTTATGGTGTTCAATGCTTTGCGAGATACCCAGATCATATGAAACAGCATGACTTTTTCAAGAGTGCCATGCCCGAAGGTTATGTACAGGAAAGAACTATATTTTTCAAAGATGACGGGAACTACAAGACACGTGCTGAAGTCAAGTTTGAAGGTGATACCCTTGTTAATAGAATCGAGTTAAAAGGTATTGATTTTAAAGAAGATGGAAACATTCTTGGACACAAATTGGAATACAACTATAACTCACACAATGTATACATCATGGCAGACAAACAAAAGAATGGAATCAAAGTTAACTTCAAAATTAGACACAACATTGAAGATGGAAGCGTTCAACTAGCAGACCATTATCAACAAAATACTCCAATTGGCGATGGCCCTGTCCTTTTACCAGACAACCATTACCTGTCCACACAATCTGCCCTTTCGAAAGATCCCAACGAAAAGAGAGATCACATGGTCCTTCTTGAGTTTGTAACAGCTGCTGGGATTACACATGGCATGGATGAACTATACAAATAATAAAGGTagagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca + + + + + E0040m_gfp_sequence + 1 + E0040m_gfp Sequence + + + + ATGCGTAAAGGAGAAGAACTTTTCACTGGAGTTGTCCCAATTCTTGTTGAATTAGATGGTGATGTTAATGGGCACAAATTTTCTGTCAGTGGAGAGGGTGAAGGTGATGCAACATACGGAAAACTTACCCTTAAATTTATTTGCACTACTGGAAAACTACCTGTTCCATGGCCAACACTTGTCACTACTTTCGGTTATGGTGTTCAATGCTTTGCGAGATACCCAGATCATATGAAACAGCATGACTTTTTCAAGAGTGCCATGCCCGAAGGTTATGTACAGGAAAGAACTATATTTTTCAAAGATGACGGGAACTACAAGACACGTGCTGAAGTCAAGTTTGAAGGTGATACCCTTGTTAATAGAATCGAGTTAAAAGGTATTGATTTTAAAGAAGATGGAAACATTCTTGGACACAAATTGGAATACAACTATAACTCACACAATGTATACATCATGGCAGACAAACAAAAGAATGGAATCAAAGTTAACTTCAAAATTAGACACAACATTGAAGATGGAAGCGTTCAACTAGCAGACCATTATCAACAAAATACTCCAATTGGCGATGGCCCTGTCCTTTTACCAGACAACCATTACCTGTCCACACAATCTGCCCTTTCGAAAGATCCCAACGAAAAGAGAGATCACATGGTCCTTCTTGAGTTTGTAACAGCTGCTGGGATTACACATGGCATGGATGAACTATACAAATAATAA + + + + + Fusion_Site_C_sequence + 1 + Fusion_Site_C Sequence + + + + AATG + + + + + Fusion_Site_D_sequence + 1 + Fusion_Site_D Sequence + + + + AGGT + + + + + dva_backbone_core_seq + 1 + + + + 26479688 + agagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca + + + + + amp_region_seq + 1 + + + + 26479688 + cgcggaacccctatttgtttatttttctaaatacattcaaatatgtatccgctcatgagacaataaccctgataaatgcttcaataatattgaaaaaggaagagtatgagtattcaacatttccgtgtcgcccttattcccttttttgcggcattttgccttcctgtttttgctcacccagaaacgctggtgaaagtaaaagatgctgaagatcagttgggtgcacgagtgggttacatcgaactggatctcaacagcggtaagatccttgagagttttcgccccgaagaacgttttccaatgatgagcacttttaaagttctgctatgtggcgcggtattatcccgtattgacgccgggcaagagcaactcggtcgccgcatacactattctcagaatgacttggttgagtactcaccagtcacagaaaagcatcttacggatggcatgacagtaagagaattatgcagtgctgccataaccatgagtgataacactgcggccaacttacttctgacaacgatcggaggaccgaaggagctaaccgcttttttgcacaacatgggggatcatgtaactcgccttgatcgttgggaaccggagctgaatgaagccataccaaacgacgagcgtgacaccacgatgcctgtagcaatggcaacaacgttgcgcaaactattaactggcgaactacttactctagcttcccggcaacaattaatagactggatggaggcggataaagttgcaggaccacttctgcgctcggcccttccggctggctggtttattgctgataaatctggagccggtgagcgtgggtcgcgcggtatcattgcagcactggggccagatggtaagccctcccgtatcgtagttatctacacgacggggagtcaggcaactatggatgaacgaaatagacagatcgctgagataggtgcctcactgattaagcattggtaa + + + + + origin_of_replication_seq + 1 + + + + 26479688 + ttgagatcctttttttctgcgcgtaatctgctgcttgcaaacaaaaaaaccaccgctaccagcggtggtttgtttgccggatcaagagctaccaactctttttccgaaggtaactggcttcagcagagcgcagataccaaatactgttcttctagtgtagccgtagttaggccaccacttcaagaactctgtagcaccgcctacatacctcgctctgctaatcctgttaccagtggctgctgccagtggcgataagtcgtgtcttaccgggttggactcaagacgatagttaccggataaggcgcagcggtcgggctgaacggggggttcgtgcacacagcccagcttggagcgaacgacctacaccgaactgagatacctacagcgtgagctatgagaaagcgccacgcttcccgaagggagaaaggcggacaggtatccggtaagcggcagggtcggaacaggagagcgcacgagggagcttccagggggaaacgcctggtatctttatagtcctgtcgggtttcgccacctctgacttgagcgtcgatttttgtgatgctcgtcaggggggcggagcctgtggaaa + + + \ No newline at end of file diff --git a/tests/test_files/pJ23100_AB.xml b/tests/test_files/pJ23100_AB.xml index d43bd38..f6174e8 100644 --- a/tests/test_files/pJ23100_AB.xml +++ b/tests/test_files/pJ23100_AB.xml @@ -4,9 +4,10 @@ pJ23100_AB 1 - pJ23100_AB + pJ23100_AB + @@ -17,6 +18,7 @@ 1 + @@ -28,6 +30,7 @@ 1 + @@ -39,6 +42,7 @@ 1 + @@ -50,6 +54,7 @@ 1 + @@ -61,6 +66,7 @@ 1 + @@ -68,6 +74,7 @@ 1 + 1 4 @@ -83,12 +90,14 @@ 1 + location1 1 + 5 39 @@ -104,6 +113,7 @@ pJ23100_ABAnnotation2 1 + @@ -111,6 +121,7 @@ location2 1 + 40 43 @@ -126,6 +137,7 @@ pJ23100_ABAnnotation3 1 + @@ -133,9 +145,10 @@ location3 1 + 44 - 2210 + 2143 @@ -149,6 +162,7 @@ 1 + @@ -161,6 +175,7 @@ 1 + @@ -173,6 +188,7 @@ 1 + @@ -188,6 +204,7 @@ MoClo standard fusion site A + 26479688 @@ -203,6 +220,7 @@ + 26479688 @@ -216,6 +234,7 @@ MoClo Basic Part: Constitutive promoter - Anderson series - high strength + 26479688 @@ -229,6 +248,7 @@ This is the backbone core for Destination Vector Ampicillin (DVA) plasmids. Based on the pSB1A2 plasmid and contains an ampicillin resistance gene and a high copy number origin of replication. + 26479688 @@ -239,6 +259,7 @@ 1 + 26479688 @@ -251,6 +272,7 @@ 1 + 26479688 @@ -263,6 +285,7 @@ 1 + 26479688 @@ -271,6 +294,7 @@ 1 + 26479688 272 860 @@ -287,6 +311,7 @@ 1 + 26479688 @@ -295,6 +320,7 @@ 1 + 26479688 1031 1996 @@ -311,10 +337,13 @@ amp_region 1 Ampicillin resistance gene from the pSB1A2 plasmid. + 2026-02-05T23:21:12 + 26479688 + @@ -323,7 +352,9 @@ origin_of_replication_pSB1A2 1 Origin of replication from the pSB1A2/pSB1K3 plasmid. pSB1A2 is a high copy number plasmid. The replication origin is a pUC19-derived pMB1 (copy number of 100-300 per cell + 2026-04-16T22:42:31 + 26479688 @@ -336,7 +367,8 @@ 1 - GGAGTTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCTACTtccagtcgggaaacctgtcgtgccagctgcattaatgaatcggccaacgcgcggggaagacgtgcttagagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca + + GGAGTTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCTACTagagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca @@ -345,6 +377,7 @@ 1 Fusion_Site_A Sequence + GGAG @@ -355,6 +388,7 @@ 1 Fusion_Site_B Sequence + TACT @@ -365,6 +399,7 @@ 1 J23100 Sequence + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGC @@ -375,6 +410,7 @@ 1 + 26479688 agagacctactagtagcggccgctgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagttaccaatgcttaatcagtgaggcacctatctcagcgatctgtctatttcgttcatccatagttgcctgactccccgtcgtgtagataactacgatacgggagggcttaccatctggccccagtgctgcaatgataccgcgcgacccacgctcaccggctccagatttatcagcaataaaccagccagccggaagggccgagcgcagaagtggtcctgcaactttatccgcctccatccagtctattaattgttgccgggaagctagagtaagtagttcgccagttaatagtttgcgcaacgttgttgccattgctacaggcatcgtggtgtcacgctcgtcgtttggtatggcttcattcagctccggttcccaacgatcaaggcgagttacatgatcccccatgttgtgcaaaaaagcggttagctccttcggtcctccgatcgttgtcagaagtaagttggccgcagtgttatcactcatggttatggcagcactgcataattctcttactgtcatgccatccgtaagatgcttttctgtgactggtgagtactcaaccaagtcattctgagaatagtgtatgcggcgaccgagttgctcttgcccggcgtcaatacgggataataccgcgccacatagcagaactttaaaagtgctcatcattggaaaacgttcttcggggcgaaaactctcaaggatcttaccgctgttgagatccagttcgatgtaacccactcgtgcacccaactgatcttcagcatcttttactttcaccagcgtttctgggtgagcaaaaacaggaaggcaaaatgccgcaaaaaagggaataagggcgacacggaaatgttgaatactcatactcttcctttttcaatattattgaagcatttatcagggttattgtctcatgagcggatacatatttgaatgtatttagaaaaataaacaaataggggttccgcgcacatttccccgaaaagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtgggtctca @@ -384,6 +420,7 @@ amp_region_seq 1 + 26479688 cgcggaacccctatttgtttatttttctaaatacattcaaatatgtatccgctcatgagacaataaccctgataaatgcttcaataatattgaaaaaggaagagtatgagtattcaacatttccgtgtcgcccttattcccttttttgcggcattttgccttcctgtttttgctcacccagaaacgctggtgaaagtaaaagatgctgaagatcagttgggtgcacgagtgggttacatcgaactggatctcaacagcggtaagatccttgagagttttcgccccgaagaacgttttccaatgatgagcacttttaaagttctgctatgtggcgcggtattatcccgtattgacgccgggcaagagcaactcggtcgccgcatacactattctcagaatgacttggttgagtactcaccagtcacagaaaagcatcttacggatggcatgacagtaagagaattatgcagtgctgccataaccatgagtgataacactgcggccaacttacttctgacaacgatcggaggaccgaaggagctaaccgcttttttgcacaacatgggggatcatgtaactcgccttgatcgttgggaaccggagctgaatgaagccataccaaacgacgagcgtgacaccacgatgcctgtagcaatggcaacaacgttgcgcaaactattaactggcgaactacttactctagcttcccggcaacaattaatagactggatggaggcggataaagttgcaggaccacttctgcgctcggcccttccggctggctggtttattgctgataaatctggagccggtgagcgtgggtcgcgcggtatcattgcagcactggggccagatggtaagccctcccgtatcgtagttatctacacgacggggagtcaggcaactatggatgaacgaaatagacagatcgctgagataggtgcctcactgattaagcattggtaa @@ -395,6 +432,7 @@ 1 + 26479688 ttgagatcctttttttctgcgcgtaatctgctgcttgcaaacaaaaaaaccaccgctaccagcggtggtttgtttgccggatcaagagctaccaactctttttccgaaggtaactggcttcagcagagcgcagataccaaatactgttcttctagtgtagccgtagttaggccaccacttcaagaactctgtagcaccgcctacatacctcgctctgctaatcctgttaccagtggctgctgccagtggcgataagtcgtgtcttaccgggttggactcaagacgatagttaccggataaggcgcagcggtcgggctgaacggggggttcgtgcacacagcccagcttggagcgaacgacctacaccgaactgagatacctacagcgtgagctatgagaaagcgccacgcttcccgaagggagaaaggcggacaggtatccggtaagcggcagggtcggaacaggagagcgcacgagggagcttccagggggaaacgcctggtatctttatagtcctgtcgggtttcgccacctctgacttgagcgtcgatttttgtgatgctcgtcaggggggcggagcctgtggaaa diff --git a/tests/test_files/transformation_activity.xml b/tests/test_files/transformation_activity.xml new file mode 100644 index 0000000..d04a835 --- /dev/null +++ b/tests/test_files/transformation_activity.xml @@ -0,0 +1,1884 @@ + + + + + + + + + + 1 + pB0015_DE_digestion_product + + + 1 + + + + + 1 + pB0015_DE_reactant + + + + + + + + + + + 1 + pE0030_CD_digestion_product + + + + + + + + pJ23100_AB_reactant + 1 + + + + + + + pB0034_BC_digestion_product + + 1 + + + + + + + + + 1 + pB0034_BC_extracted_part_ligation + + + + + + composite_1_ligation + + + + + + + pE0030_CD_extracted_part_ligation + 1 + + + 1 + + + 1 + + + + composite_1_product + + + + + + pB0015_DE_extracted_part_ligation + + + 1 + + + + + 1 + + pJ23100_AB_extracted_part_ligation + + + + + + + + + 1 + + DVK_AE_extracted_backbone_ligation + + + + + + + + + 1 + + + composite_1 + + + + + + 1 + + + + + pB0034_BC_reactant + + + + + + 1 + + + + DVK_AE_backbone_reactant + + + + + + + + 1 + + BsaI_enzyme + + + + + + 1 + + + + 1 + + + DVK_AE_backbone_product + + + + + + + + restriction + 1 + + + + DVK_AE_digestion + + + + 1 + + + DVK_AE_backbone_reactant + + + + + + + + T4_Ligase + + + 1 + + + + + + + + restriction + 1 + + + + + + + + + + 1 + pB0034_BC_product + + + + 1 + pB0034_BC_digestion + + + + + 1 + pB0034_BC_reactant + + + + + + + + + + DVK_AE_backbone_digestion_product + + + 1 + + + + + + + + 1 + pJ23100_AB_digestion_product + + + + + + + + 1 + pJ23100_AB_digestion + + + + 1 + pJ23100_AB_product + + + + + + + + + + + 1 + pJ23100_AB_reactant + + + + + + + + 1 + + restriction + + + + + + + + pE0030_CD_reactant + + + 1 + + + + assembly_plan + + + pE0030_CD_digestion + + + + + + 1 + pE0030_CD_product + + + + + + + 1 + + + pE0030_CD_reactant + + + + + + + + restriction + 1 + + + + 1 + + + + + 1 + + + + 1 + + + pB0015_DE_product + + + pB0015_DE_digestion + + + + + restriction + 1 + + + + + + + + + + 1 + + pB0015_DE_reactant + + + + + + + + Escherichia coli, strain DH5alpha + + 1 + Ecoli_DH5a + + + Ecoli_DH5a + + + + + 1 + + + + composite_1_engineered_plasmid + + + + + 1 + + + + + Ecoli_DH5a_chassis + + + 1 + + + Ecoli_DH5a_with_composite_1 + + + + + BsaI + BsaI + Restriction enzyme BsaI from REBASE. + + + 1 + + + + + + pJ23100_AB_five_prime_oh_component + + 1 + + + + + + + + + + + 1 + + three_prime_oh_location + 40 + 43 + + + three_prime_overhang + + 1 + + + + 1 + + pJ23100_AB_extracted_part + + + + + 1 + J23100_3 + + + + + + + + + + + 1 + pJ23100_AB_part + + + 5 + 39 + + 1 + + pJ23100_AB_part_location + + + + + + + + + 1 + + + + pJ23100_AB_three_prime_oh_component + + + + + + + 1 + + + 1 + 1 + + 4 + + five_prime_oh_location + + + + five_prime_overhang + + + + + + 1 + + pJ23100_AB_five_prime_oh + + + + + + + + + 1 + Fusion_Site_A + MoClo standard fusion site A + + Fusion_Site_A + 26479688 + + + + + + + MoClo Basic Part: Constitutive promoter - Anderson series - high strength + + + J23100 + + + + + J23100 + + + 26479688 + 1 + + + pJ23100_AB_three_prime_oh + + + + + 1 + + + + + + + + 26479688 + Fusion_Site_B + Fusion_Site_B + + MoClo standard fusion site B + + + + 1 + + + + + + + 1 + + + B0034_3 + + + + + + + + 1 + five_prime_overhang + + + + 1 + 1 + five_prime_oh_location + + + 4 + + + + + + + + + + 1 + + 26 + three_prime_oh_location + 29 + + + + three_prime_overhang + 1 + + + + + + + + + + 1 + + + + 1 + 25 + 5 + + pB0034_BC_part_location + + + pB0034_BC_part + + + + + + + 1 + + pB0034_BC_five_prime_oh_component + + + + + + 1 + pB0034_BC_three_prime_oh_component + + + + + + 1 + pB0034_BC_extracted_part + + + + + + + + + + 1 + + pB0034_BC_five_prime_oh + + + + + + + B0034 + + B0034 + MoClo Basic Part: RBS - Weiss RBS, high strength. Modified from Bba_B0034 to adjust spacing in MC system. + 26479688 + 1 + + + + + + + + 1 + pB0034_BC_three_prime_oh + + + + + + Fusion_Site_C + 1 + Fusion_Site_C + MoClo standard fusion site C + + + 26479688 + + + + + + + + + + + + + 728 + three_prime_oh_location + + + 731 + 1 + + + 1 + three_prime_overhang + + + + + 1 + + + + + + 1 + + + + 727 + pE0030_CD_part_location + 1 + + 5 + + + pE0030_CD_part + + + + + + + + pE0030_CD_five_prime_oh_component + + 1 + + + + pE0030_CD_extracted_part + + + 1 + + + E0030_yfp_3 + + + + + + + + + + pE0030_CD_three_prime_oh_component + + + + 1 + + + + + + 1 + + + + + 4 + 1 + five_prime_oh_location + + 1 + + + + five_prime_overhang + + + + + + pE0030_CD_five_prime_oh + + + 1 + + + + + + 26479688 + + + MoClo Basic Part: CDS - Fluorescent protein. Yellow. + + + + E0030_yfp + + + E0030_yfp + 1 + + + + + + + pE0030_CD_three_prime_oh + + + 1 + + + + + + MoClo standard fusion site D + + 1 + + Fusion_Site_D + + 26479688 + + Fusion_Site_D + + + + 1 + + + three_prime_overhang + + + 1 + + + 137 + + + 1 + 134 + three_prime_oh_location + + + + + + + + pB0015_DE_five_prime_oh_component + + + 1 + + + + + pB0015_DE_three_prime_oh_component + + 1 + + + + + + + + + + 1 + + B0015_3 + + + + + + + + + 1 + + + 4 + + + 1 + five_prime_oh_location + 1 + + + five_prime_overhang + + + + + + + + + pB0015_DE_extracted_part + + + pB0015_DE_part + + + + + 5 + pB0015_DE_part_location + 133 + + 1 + + + + 1 + + + + + + + + 1 + + + pB0015_DE_five_prime_oh + + + B0015 + + 26479688 + MoClo Basic Part: Double terminator (B0010:B0012) + + + + + + + 1 + B0015 + + + + + + 1 + + pB0015_DE_three_prime_oh + + + + + + Fusion_Site_E + + + 26479688 + + + 1 + + MoClo standard fusion site E + + + Fusion_Site_E + + + DVK_AE_extracted_backbone + + 1 + + + + DVK_AE_five_prime_oh_component + 1 + + + + + + + + + + + + + 1 + three_prime_oh_location + 2232 + 2235 + + + three_prime_overhang + 1 + + + + + + + + + + + 1 + + + dvk_backbone_core_5 + + + + + DVK_AE_backbone + + + + + 5 + 1 + DVK_AE_backbone_location + 2231 + + + + + 1 + + + + + DVK_AE_three_prime_oh_component + + 1 + + + + + + + + five_prime_overhang + + + 1 + five_prime_oh_location + + 1 + + 4 + + + + 1 + + + + + + + + DVK_AE_three_prime_oh + + + + + 1 + + + 1 + + DVK_AE_five_prime_oh + + + + + + + This is the backbone core for Destination Vector Kanamycin (DVK) plasmids. Based on the pSB1K3 plasmid and contains kanamycin gene and a high copy number origin of replication. + dvk_backbone_core + 26479688 + + 1 + + + kan_region_annotation + + + 1 + + + kan + 1929 + + 1103 + 26479688 + + + + + + + + 1 + + + 26479688 + + + + + 26479688 + + + + + + 1 + Component_ori + + + + + + + + + + + 870 + + + 1 + + + ori + + 282 + 26479688 + + + + + + 1 + origin_of_replication_pSB1A2_annotation + + + 26479688 + + + + + + 1 + Component_kan + + + 26479688 + + + + + + + + + + + + T4_Ligase + 1 + + T4_Ligase + + + Ligation_Scar_A + + + + + 1 + + + + + + 1 + Ligation_Scar_B + + + + + + + + + + + Ligation_Scar_C + + 1 + + + + + + + + 1 + + Ligation_Scar_D + + + + + + + 1 + + Ligation_Scar_E + + + + + + + + + + + 1 + + Ligation_Scar_E + + + + + 1 + Ligation_Scar_D + + + + + + + + dvk_backbone_core + + + + 1 + + + + + + + + 1 + + Ligation_Scar_A + + + + + + + 1 + + + 2270 + J23100_3_location + + + 1 + 2236 + + + J23100_3_annotation + + + + + Ligation_Scar_E_annotation + + + + + + 1 + 3026 + 3023 + Ligation_Scar_E_location + + + + 1 + + + + + + Ligation_Scar_C + + + 1 + + + composite_1 + + + + + E0030_yfp_3_annotation + + + 3022 + 1 + E0030_yfp_3_location + + + 2300 + + + 1 + + + + + 1 + + + B0015_3_annotation + + + 3155 + + 3027 + 1 + B0015_3_location + + + + + + + + 1 + + + E0030_yfp + + + + + composite_1 + + + + + Ligation_Scar_A_annotation + 1 + + + + + 1 + Ligation_Scar_A_location + 1 + 4 + + + + + + + 1 + + dvk_backbone_core_5_annotation + + + + 1 + + dvk_backbone_core_5_location + 5 + 2231 + + + + + + + + + + + + Ligation_Scar_D_location + 2299 + 2296 + 1 + + + 1 + + Ligation_Scar_D_annotation + + + + + + + + + J23100 + 1 + + + + + + + + B0034 + 1 + + + + 1 + + + B0034_3_annotation + + + B0034_3_location + + 2275 + 1 + 2295 + + + + + + 1 + + + + + + + Ligation_Scar_B_annotation + + + 2235 + + + Ligation_Scar_B_location + 2232 + 1 + + + 1 + + + + + + + + + B0015 + + 1 + + + + + + 1 + Ligation_Scar_B + + + + + + + + + Ligation_Scar_C_annotation + 1 + + + + 2274 + + 1 + Ligation_Scar_C_location + 2271 + + + + + + + + + GGAGTTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCTACT + 1 + pJ23100_AB_extracted_part_seq + + + 1 + + + pJ23100_AB_five_prime_oh_sequence + GGAG + + + + GGAG + + + + 1 + Fusion_Site_A Sequence + + Fusion_Site_A_sequence + + + + J23100_sequence + + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGC + + + J23100 Sequence + + 1 + + + + + + + 1 + pJ23100_AB_three_prime_oh_sequence + TACT + + + + Fusion_Site_B Sequence + + 1 + TACT + + + Fusion_Site_B_sequence + + + + 1 + pB0034_BC_extracted_part_seq + TACTAGAGAAAGAGGAGAAATACTAAATG + + + + + 1 + TACT + + + pB0034_BC_five_prime_oh_sequence + + + + + B0034_sequence + + AGAGAAAGAGGAGAAATACTA + + + B0034 Sequence + 1 + + + + 1 + + pB0034_BC_three_prime_oh_sequence + + AATG + + + + Fusion_Site_C Sequence + AATG + + 1 + Fusion_Site_C_sequence + + + + + + + 1 + AATGATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCTTCGGCTACGGCCTGCAATGCTTCGCCCGCTACCCCGACCACATGAAGCTGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCTACCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAATAAAGGT + + pE0030_CD_extracted_part_seq + + + + + pE0030_CD_five_prime_oh_sequence + AATG + + 1 + + + + + 1 + + E0030_yfp_sequence + + + ATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCTTCGGCTACGGCCTGCAATGCTTCGCCCGCTACCCCGACCACATGAAGCTGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCTACCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAATAA + + E0030_yfp Sequence + + + + + pE0030_CD_three_prime_oh_sequence + AGGT + 1 + + + + AGGT + 1 + Fusion_Site_D_sequence + + Fusion_Site_D Sequence + + + + + + + + pB0015_DE_extracted_part_seq + 1 + AGGTCCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATAGCTT + + + + + + 1 + + AGGT + pB0015_DE_five_prime_oh_sequence + + + + B0015 Sequence + CCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + 1 + + B0015_sequence + + + + GCTT + + + pB0015_DE_three_prime_oh_sequence + 1 + + + + + + 1 + + + Fusion_Site_E Sequence + GCTT + + Fusion_Site_E_sequence + + + + + GCTTatgtcttctactagtagcggccgctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtggaagacatGGAG + DVK_AE_extracted_backbone_seq + 1 + + + + DVK_AE_three_prime_oh_sequence + GGAG + 1 + + + + + GCTT + + + 1 + + DVK_AE_five_prime_oh_sequence + + + atgtcttctactagtagcggccgctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtggaagacat + 26479688 + 1 + + + + + dvk_backbone_core_seq + + + + 1 + Ligation_Scar_A_sequence + GCTT + + + + + + Ligation_Scar_B_sequence + GGAG + + 1 + + + Ligation_Scar_C_sequence + + + 1 + TACT + + + + AATG + + Ligation_Scar_D_sequence + 1 + + + 1 + + + Ligation_Scar_E_sequence + AGGT + + + 1 + GCTTatgtcttctactagtagcggccgctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtggaagacatGGAGTTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCTACTAGAGAAAGAGGAGAAATACTAAATGATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCTTCGGCTACGGCCTGCAATGCTTCGCCCGCTACCCCGACCACATGAAGCTGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCTACCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAATAAAGGTCCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + composite_1_seq + + + + + composite_1_assembly_plan + 1 + + + + Ecoli_DH5a_with_composite_1_transformation_plan + TODO: generate accurate description of transformation + + 1 + + + buildcompiler + 1 + + + + + + + + + assemble_composite_1_design + 1 + + + + Golden Gate Assembly + assemble_composite_1 + 1 + + + + + 1 + + + assemble_composite_1_association + + + + + Bacterial Tranformation + 1 + transform_Ecoli_DH5a + + + + Ecoli_DH5a_chassis_source + + + 1 + + + + + + 1 + + + + transform_Ecoli_DH5a_association + + + + + + composite_1_plasmid_source + 1 + + + + + + + diff --git a/tests/test_files/transformation_activity_new.xml b/tests/test_files/transformation_activity_new.xml new file mode 100644 index 0000000..8f51315 --- /dev/null +++ b/tests/test_files/transformation_activity_new.xml @@ -0,0 +1,1884 @@ + + + + + + T4_Ligase + 1 + + + + + + + + + pE0030_CD_reactant + + + 1 + + + + + + 1 + + + + + pB0015_DE_reactant + + + + + + + + + 1 + DVK_AE_backbone_digestion_product + + + + + + 1 + pJ23100_AB_digestion_product + + + + + + + + + + + + 1 + pE0030_CD_extracted_part_ligation + + + + + + + + 1 + + composite_1_product + + + + + + + + + 1 + pB0034_BC_extracted_part_ligation + + + + + + pJ23100_AB_extracted_part_ligation + + + 1 + + + 1 + composite_1_ligation + + + + + + 1 + + DVK_AE_extracted_backbone_ligation + + + + + + + pB0015_DE_extracted_part_ligation + + 1 + + + + + + + pB0015_DE_digestion + + + 1 + + pB0015_DE_product + + + + + + + + + + 1 + pB0015_DE_reactant + + + + 1 + + + + 1 + restriction + + + + + + + + + + 1 + + + + DVK_AE_backbone_reactant + + + + + + + + pB0015_DE_digestion_product + + 1 + + + + 1 + + + BsaI_enzyme + + + + + 1 + + + + + + DVK_AE_digestion + + + + DVK_AE_backbone_reactant + + 1 + + + + + + 1 + + restriction + + + + + 1 + + + + DVK_AE_backbone_product + + + + 1 + + + + + + + 1 + + + + pJ23100_AB_reactant + + + + + + + + 1 + pJ23100_AB_reactant + + + + + + 1 + + + + + 1 + restriction + + + + + + + + + pJ23100_AB_product + + 1 + + + + pJ23100_AB_digestion + + + + + pB0034_BC_digestion + + + + 1 + + restriction + + + + + 1 + + + 1 + + + + pB0034_BC_product + + + + + + pB0034_BC_reactant + + + + 1 + + + + + assembly_plan + + + + pE0030_CD_digestion_product + + + 1 + + + + + + + 1 + composite_1 + + + + + + + + + 1 + + + pB0034_BC_digestion_product + + + + + + + + + + restriction + + 1 + + + + 1 + + + + + pE0030_CD_reactant + + 1 + + + + + + + + + pE0030_CD_product + 1 + + + + pE0030_CD_digestion + + + + + + 1 + pB0034_BC_reactant + + + + + + + + 1 + Escherichia coli, strain DH5alpha + + + Ecoli_DH5a + Ecoli_DH5a + + + + + + + + + + + 1 + + composite_1_engineered_plasmid + + + + 1 + + + + + 1 + + Ecoli_DH5a_chassis + + + Ecoli_DH5a_with_composite_1 + + + + 1 + + + BsaI + Restriction enzyme BsaI from REBASE. + BsaI + + + + + + + + pJ23100_AB_five_prime_oh_component + + 1 + + + + + + pJ23100_AB_part + 1 + + + + 5 + + 1 + 39 + + pJ23100_AB_part_location + + + + + + + + + + + + + + J23100_3 + 1 + + + + pJ23100_AB_extracted_part + + + + 1 + pJ23100_AB_three_prime_oh_component + + + + + + + + + five_prime_overhang + + + + 4 + five_prime_oh_location + + + 1 + 1 + + + + 1 + + + + + + + three_prime_overhang + + + 43 + + 1 + + 40 + three_prime_oh_location + + + 1 + + + 1 + + + + + + + + 1 + + pJ23100_AB_five_prime_oh + + + + + + Fusion_Site_A + MoClo standard fusion site A + + Fusion_Site_A + + + + 1 + + 26479688 + + + + + + 1 + + J23100 + + + J23100 + + + + MoClo Basic Part: Constitutive promoter - Anderson series - high strength + 26479688 + + + + pJ23100_AB_three_prime_oh + + + + + 1 + + + + + Fusion_Site_B + + + + 26479688 + + + + 1 + Fusion_Site_B + MoClo standard fusion site B + + + + + + + + + 1 + B0034_3 + + + + + + + + + + 1 + + 26 + 29 + three_prime_oh_location + + + + 1 + + three_prime_overhang + + + + + pB0034_BC_extracted_part + + + + + + + 1 + + 1 + 4 + five_prime_oh_location + + + + five_prime_overhang + + 1 + + + + 1 + + + + 1 + + + 25 + + pB0034_BC_part_location + 5 + + 1 + + + pB0034_BC_part + + + + + + + + pB0034_BC_five_prime_oh_component + 1 + + + + + + + + 1 + pB0034_BC_three_prime_oh_component + + + + + + + + + 1 + + pB0034_BC_five_prime_oh + + + + + + + B0034 + + + + 26479688 + + + + 1 + + MoClo Basic Part: RBS - Weiss RBS, high strength. Modified from Bba_B0034 to adjust spacing in MC system. + B0034 + + + + + pB0034_BC_three_prime_oh + 1 + + + + + + Fusion_Site_C + Fusion_Site_C + 1 + + MoClo standard fusion site C + + + 26479688 + + + + + + + + + + 1 + + + pE0030_CD_part + + + 727 + + 5 + + pE0030_CD_part_location + 1 + + + + + + + + five_prime_overhang + + + + 4 + five_prime_oh_location + + 1 + 1 + + + 1 + + + + + + + + pE0030_CD_five_prime_oh_component + 1 + + + + pE0030_CD_extracted_part + + + + + 1 + + + + pE0030_CD_three_prime_oh_component + + + + 1 + + + + + + + E0030_yfp_3 + 1 + + + + + + + + + 1 + three_prime_overhang + + + 731 + 728 + + + 1 + three_prime_oh_location + + + + + + + + + + + pE0030_CD_five_prime_oh + + + + + 1 + + + E0030_yfp + + E0030_yfp + + + + 26479688 + + + MoClo Basic Part: CDS - Fluorescent protein. Yellow. + 1 + + + + + pE0030_CD_three_prime_oh + 1 + + + + + + + + + 26479688 + + Fusion_Site_D + Fusion_Site_D + + + + 1 + MoClo standard fusion site D + + + + + + + + + + three_prime_overhang + + + + three_prime_oh_location + + 134 + 1 + 137 + + + + 1 + + + + + + 1 + + + + pB0015_DE_three_prime_oh_component + + + 1 + pB0015_DE_extracted_part + + + 1 + + pB0015_DE_five_prime_oh_component + + + + + + + + 1 + + five_prime_overhang + + + + five_prime_oh_location + 1 + 1 + + 4 + + + + + + + + pB0015_DE_part + + + + + pB0015_DE_part_location + + + 133 + 5 + 1 + + + 1 + + + + + + 1 + + + + + + + B0015_3 + + + + + + + + + pB0015_DE_five_prime_oh + + + 1 + + + + 26479688 + 1 + + + B0015 + + MoClo Basic Part: Double terminator (B0010:B0012) + + + + + B0015 + + + + + + + + + 1 + pB0015_DE_three_prime_oh + + + Fusion_Site_E + 26479688 + + Fusion_Site_E + + + + 1 + MoClo standard fusion site E + + + + + + + + + + 1 + + + + 5 + DVK_AE_backbone_location + + 2231 + 1 + + + + DVK_AE_backbone + + + + + + + DVK_AE_five_prime_oh_component + + + 1 + + + + + + + + three_prime_oh_location + 2235 + + 2232 + 1 + + + + three_prime_overhang + + + 1 + + + + + 1 + + + + + + 4 + 1 + five_prime_oh_location + 1 + + + five_prime_overhang + + + + + + 1 + + + + + + dvk_backbone_core_5 + + + + + DVK_AE_extracted_backbone + + + + + + DVK_AE_three_prime_oh_component + + + 1 + + + 1 + + + + + 1 + + + DVK_AE_three_prime_oh + + + + + + + DVK_AE_five_prime_oh + + 1 + + + + + + kan_region_annotation + 26479688 + + + + + 1103 + 26479688 + + + kan + + 1929 + 1 + + + + + + + + 1 + + + + + + + + 1 + + + 870 + + 282 + + + + ori + 26479688 + 1 + + + + + origin_of_replication_pSB1A2_annotation + + 26479688 + + + dvk_backbone_core + + + + 1 + 26479688 + + + + + + + Component_ori + + + + + 26479688 + 1 + + + + 26479688 + + + Component_kan + + 1 + + + + + + This is the backbone core for Destination Vector Kanamycin (DVK) plasmids. Based on the pSB1K3 plasmid and contains kanamycin gene and a high copy number origin of replication. + + + + + + T4_Ligase + 1 + + T4_Ligase + + + + + + + + + Ligation_Scar_A + 1 + + + + + + + + Ligation_Scar_B + 1 + + + + + + + + 1 + + + Ligation_Scar_C + + + + + + + + + Ligation_Scar_D + 1 + + + + + 1 + + + + + + Ligation_Scar_E + + + + + + + Ligation_Scar_E + + 1 + + + + + + + B0015 + + 1 + + + + + 1 + + Ligation_Scar_B_annotation + + + + 2232 + + Ligation_Scar_B_location + + 1 + 2235 + + + + + + + 1 + + Ligation_Scar_B + + + + + + + + + Ligation_Scar_A + 1 + + + + + + + + + + Ligation_Scar_D + 1 + + + 1 + + + B0034 + + + 1 + + + + composite_1 + + + + 1 + Ligation_Scar_C + + + + + + + 1 + + + + 1 + 2231 + 5 + + + dvk_backbone_core_5_location + + + + dvk_backbone_core_5_annotation + + + + + Ligation_Scar_A_annotation + + + 1 + Ligation_Scar_A_location + 4 + + + 1 + + + + + 1 + + + + + + + + 3155 + B0015_3_location + + 3027 + 1 + + + + B0015_3_annotation + + 1 + + + + + + 1 + + + + 1 + + 2270 + 2236 + + J23100_3_location + + + J23100_3_annotation + + + + + + Ligation_Scar_C_annotation + + + 1 + + + + 1 + 2274 + 2271 + + Ligation_Scar_C_location + + + + + + + + 1 + + E0030_yfp + + + + + + + + + Ligation_Scar_E_annotation + + + 3023 + Ligation_Scar_E_location + + 3026 + 1 + + + + 1 + + + + + + + J23100 + 1 + + + + + + composite_1 + + + dvk_backbone_core + + 1 + + + + + + + + + 1 + Ligation_Scar_D_location + + + 2296 + 2299 + + + + Ligation_Scar_D_annotation + + 1 + + + + + + + + + + 1 + E0030_yfp_3_location + 3022 + 2300 + + + 1 + + E0030_yfp_3_annotation + + + + + + B0034_3_annotation + + + 2275 + 2295 + + B0034_3_location + + 1 + + + + 1 + + + + + + + 1 + pJ23100_AB_extracted_part_seq + GGAGTTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCTACT + + + + + 1 + + pJ23100_AB_five_prime_oh_sequence + GGAG + + + + Fusion_Site_A_sequence + Fusion_Site_A Sequence + + + + GGAG + + + 1 + + + + + 1 + J23100_sequence + + J23100 Sequence + + + TTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGC + + + + pJ23100_AB_three_prime_oh_sequence + TACT + + + 1 + + + Fusion_Site_B_sequence + + Fusion_Site_B Sequence + + TACT + + 1 + + + + + + 1 + pB0034_BC_extracted_part_seq + + TACTAGAGAAAGAGGAGAAATACTAAATG + + + pB0034_BC_five_prime_oh_sequence + + + TACT + + 1 + + + + + B0034_sequence + + 1 + AGAGAAAGAGGAGAAATACTA + B0034 Sequence + + + + + 1 + AATG + + pB0034_BC_three_prime_oh_sequence + + + + + Fusion_Site_C Sequence + + + AATG + Fusion_Site_C_sequence + + + + 1 + + + + + AATGATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCTTCGGCTACGGCCTGCAATGCTTCGCCCGCTACCCCGACCACATGAAGCTGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCTACCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAATAAAGGT + 1 + pE0030_CD_extracted_part_seq + + + AATG + + pE0030_CD_five_prime_oh_sequence + + + 1 + + + + E0030_yfp Sequence + + 1 + + ATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCTTCGGCTACGGCCTGCAATGCTTCGCCCGCTACCCCGACCACATGAAGCTGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCTACCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAATAA + + + E0030_yfp_sequence + + + + pE0030_CD_three_prime_oh_sequence + 1 + + AGGT + + + + + + 1 + + Fusion_Site_D Sequence + Fusion_Site_D_sequence + + AGGT + + + + + 1 + + AGGTCCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATAGCTT + pB0015_DE_extracted_part_seq + + + pB0015_DE_five_prime_oh_sequence + 1 + + + AGGT + + + + + + + + + 1 + B0015_sequence + B0015 Sequence + CCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + + + 1 + + GCTT + + + pB0015_DE_three_prime_oh_sequence + + + 1 + GCTT + + + + + Fusion_Site_E_sequence + Fusion_Site_E Sequence + + + + 1 + DVK_AE_extracted_backbone_seq + GCTTatgtcttctactagtagcggccgctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtggaagacatGGAG + + + + + + 1 + + + DVK_AE_three_prime_oh_sequence + GGAG + + + 1 + DVK_AE_five_prime_oh_sequence + + + GCTT + + + + + 26479688 + 1 + + + dvk_backbone_core_seq + atgtcttctactagtagcggccgctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtggaagacat + + + + + + 1 + GCTT + + Ligation_Scar_A_sequence + + + + Ligation_Scar_B_sequence + GGAG + + 1 + + + + TACT + + 1 + Ligation_Scar_C_sequence + + + Ligation_Scar_D_sequence + 1 + + + AATG + + + Ligation_Scar_E_sequence + 1 + + AGGT + + + + + + composite_1_seq + GCTTatgtcttctactagtagcggccgctgcagtccggcaaaaaagggcaaggtgtcaccaccctgccctttttctttaaaaccgaaaagattacttcgcgttatgcaggcttcctcgctcactgactcgctgcgctcggtcgttcggctgcggcgagcggtatcagctcactcaaaggcggtaatacggttatccacagaatcaggggataacgcaggaaagaacatgtgagcaaaaggccagcaaaaggccaggaaccgtaaaaaggccgcgttgctggcgtttttccacaggctccgcccccctgacgagcatcacaaaaatcgacgctcaagtcagaggtggcgaaacccgacaggactataaagataccaggcgtttccccctggaagctccctcgtgcgctctcctgttccgaccctgccgcttaccggatacctgtccgcctttctcccttcgggaagcgtggcgctttctcatagctcacgctgtaggtatctcagttcggtgtaggtcgttcgctccaagctgggctgtgtgcacgaaccccccgttcagcccgaccgctgcgccttatccggtaactatcgtcttgagtccaacccggtaagacacgacttatcgccactggcagcagccactggtaacaggattagcagagcgaggtatgtaggcggtgctacagagttcttgaagtggtggcctaactacggctacactagaagaacagtatttggtatctgcgctctgctgaagccagttaccttcggaaaaagagttggtagctcttgatccggcaaacaaaccaccgctggtagcggtggtttttttgtttgcaagcagcagattacgcgcagaaaaaaaggatctcaagaagatcctttgatcttttctacggggtctgacgctcagtggaacgaaaactcacgttaagggattttggtcatgagattatcaaaaaggatcttcacctagatccttttaaattaaaaatgaagttttaaatcaatctaaagtatatatgagtaaacttggtctgacagctcgagtcccgtcaagtcagcgtaatgctctgccagtgttacaaccaattaaccaattctgattagaaaaactcatcgagcatcaaatgaaactgcaatttattcatatcaggattatcaataccatatttttgaaaaagccgtttctgtaatgaaggagaaaactcaccgaggcagttccataggatggcaagatcctggtatcggtctgcgattccgactcgtccaacatcaatacaacctattaatttcccctcgtcaaaaataaggttatcaagtgagaaatcaccatgagtgacgactgaatccggtgagaatggcaaaagcttatgcatttctttccagacttgttcaacaggccagccattacgctcgtcatcaaaatcactcgcatcaaccaaaccgttattcattcgtgattgcgcctgagcgagacgaaatacgcgatcgctgttaaaaggacaattacaaacaggaatcgaatgcaaccggcgcaggaacactgccagcgcatcaacaatattttcacctgaatcaggatattcttctaatacctggaatgctgttttcccggggatcgcagtggtgagtaaccatgcatcatcaggagtacggataaaatgcttgatggtcggaagaggcataaattccgtcagccagtttagtctgaccatctcatctgtaacatcattggcaacgctacctttgccatgtttcagaaacaactctggcgcatcgggcttcccatacaatcgatagattgtcgcacctgattgcccgacattatcgcgagcccatttatacccatataaatcagcatccatgttggaatttaatcgcggcctggagcaagacgtttcccgttgaatatggctcataacaccccttgtattactgtttatgtaagcagacagttttattgttcatgatgatatatttttatcttgtgcaatgtaacatcagagattttgagacacaacgtggctttgttgaataaatcgaacttttgctgagttgaaggatcagctcgagtgccacctgacgtctaagaaaccattattatcatgacattaacctataaaaataggcgtatcacgaggcagaatttcagataaaaaaaatccttagctttcgctaaggatgatttctggaattcgcggccgcttctagagactagtggaagacatGGAGTTGACGGCTAGCTCAGTCCTAGGTACAGTGCTAGCTACTAGAGAAAGAGGAGAAATACTAAATGATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGGGTGGTGCCCATCCTGGTCGAGCTGGACGGCGACGTAAACGGCCACAAGTTCAGCGTGTCCGGCGAGGGCGAGGGCGATGCCACCTACGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACCCTCGTGACCACCTTCGGCTACGGCCTGCAATGCTTCGCCCGCTACCCCGACCACATGAAGCTGCACGACTTCTTCAAGTCCGCCATGCCCGAAGGCTACGTCCAGGAGCGCACCATCTTCTTCAAGGACGACGGCAACTACAAGACCCGCGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACCGCATCGAGCTGAAGGGCATCGACTTCAAGGAGGACGGCAACATCCTGGGGCACAAGCTGGAGTACAACTACAACAGCCACAACGTCTATATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCCGCCACAACATCGAGGACGGCAGCGTGCAGCTCGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCTACCAGTCCGCCCTGAGCAAAGACCCCAACGAGAAGCGCGATCACATGGTCCTGCTGGAGTTCGTGACCGCCGCCGGGATCACTCTCGGCATGGACGAGCTGTACAAGTAATAAAGGTCCAGGCATCAAATAAAACGAAAGGCTCAGTCGAAAGACTGGGCCTTTCGTTTTATCTGTTGTTTGTCGGTGAACGCTCTCTACTAGAGTCACACTGGCTCACCTTCGGGTGGGCCTTTCTGCGTTTATA + 1 + + + 1 + composite_1_assembly_plan + + + + Ecoli_DH5a_with_composite_1_transformation_plan + + 1 + TODO: generate accurate description of transformation + + + 1 + + buildcompiler + + + + + + + + assemble_composite_1_design + + + 1 + + + Golden Gate Assembly + assemble_composite_1 + 1 + + + + 1 + + assemble_composite_1_association + + + + + + transform_Ecoli_DH5a + + + + + + 1 + composite_1_plasmid_source + + + + + + + transform_Ecoli_DH5a_association + + 1 + + + + + + + + + Ecoli_DH5a_chassis_source + 1 + + + + Bacterial Tranformation + 1 + + From 1c67d56b32ef382ff8b1a4dc040613f871e0ba10 Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Mon, 18 May 2026 12:01:43 -0600 Subject: [PATCH 89/93] notebook updates --- notebooks/comb_design.ipynb | 10 +- notebooks/impl_creation.ipynb | 261 +++++++++++++++++----------------- 2 files changed, 138 insertions(+), 133 deletions(-) diff --git a/notebooks/comb_design.ipynb b/notebooks/comb_design.ipynb index 493be94..dcc14c7 100644 --- a/notebooks/comb_design.ipynb +++ b/notebooks/comb_design.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": 5, "id": "814099af", "metadata": {}, "outputs": [], @@ -13,7 +13,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 6, "id": "2f1ab216", "metadata": {}, "outputs": [ @@ -21,7 +21,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "\n" + "https://sbolcanvas.org/combinatorialRBSs\n" ] } ], @@ -29,7 +29,7 @@ "abstract_doc = sbol2.Document()\n", "abstract_doc.read(\"tests/test_files/combinatorial_1.xml\")\n", "comb_design = abstract_doc.combinatorialderivations[0]\n", - "print(type(comb_design))" + "print(comb_design.identity)" ] }, { @@ -58,7 +58,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "c3bbf89a", "metadata": {}, "outputs": [ diff --git a/notebooks/impl_creation.ipynb b/notebooks/impl_creation.ipynb index a87de5e..6105579 100644 --- a/notebooks/impl_creation.ipynb +++ b/notebooks/impl_creation.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 6, + "execution_count": 15, "id": "87bdb42e", "metadata": {}, "outputs": [], @@ -13,7 +13,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 16, "id": "90648527", "metadata": {}, "outputs": [ @@ -26,7 +26,7 @@ } ], "source": [ - "auth = \"0b2dc76c-4c1d-4ee8-b339-6a3e15e3faf9\"\n", + "auth = \"839935f0-7f6a-4c83-b1f0-91ddc0eb9c9a\"\n", "buildcompiler = BuildCompiler(\n", " [\n", " \"https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/CIDARMoCloPlasmidsKit_collection/1\"\n", @@ -39,7 +39,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 17, "id": "99d093a8", "metadata": {}, "outputs": [ @@ -48,84 +48,76 @@ "output_type": "stream", "text": [ "[Plasmid:\n", - " Name: pB0033_BC_B_C\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0033_BC/1\n", + " Name: pB0034_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0034_BC/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", " Fusion Sites: ['B', 'C']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: DVA_CD_C_D\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_CD/1\n", - " Strain Definitions: [None]\n", - " Plasmid Implementations: None\n", - " Strain Implementations: None\n", - " Fusion Sites: ['C', 'D']\n", - " Antibiotic Resistance: Ampicillin\n", - ", Plasmid:\n", - " Name: pJ23100_AB_A_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_AB/1\n", + " Name: pJ23100_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_GB/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['A', 'B']\n", + " Fusion Sites: ['G', 'B']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pJ23106_AB_A_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_AB/1\n", + " Name: DVA_DF_D_F\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_DF/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['A', 'B']\n", + " Fusion Sites: ['D', 'F']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pJ23116_GB_G_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_GB/1\n", + " Name: DVA_DG_D_G\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_DG/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['G', 'B']\n", + " Fusion Sites: ['D', 'G']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: DVA_DH_D_H\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_DH/1\n", + " Name: pB0015_DH_D_H\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DH/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", " Fusion Sites: ['D', 'H']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pB0015_DE_D_E\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DE/1\n", + " Name: pB0032_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0032_BC/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['D', 'E']\n", + " Fusion Sites: ['B', 'C']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pJ23100_GB_G_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_GB/1\n", + " Name: pE1010_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE1010_CD/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['G', 'B']\n", + " Fusion Sites: ['C', 'D']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pB0015_DH_D_H\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DH/1\n", + " Name: DVA_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_CD/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['D', 'H']\n", + " Fusion Sites: ['C', 'D']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: DVA_AB_A_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_AB/1\n", + " Name: DVA_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_GB/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['A', 'B']\n", + " Fusion Sites: ['G', 'B']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", " Name: pJ23116_FB_F_B\n", @@ -136,20 +128,20 @@ " Fusion Sites: ['F', 'B']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pB0015_DG_D_G\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DG/1\n", + " Name: pB0015_DE_D_E\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DE/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['D', 'G']\n", + " Fusion Sites: ['D', 'E']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pE1010_CD_C_D\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE1010_CD/1\n", + " Name: pJ23100_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_EB/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['C', 'D']\n", + " Fusion Sites: ['E', 'B']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", " Name: pE0040_CD_C_D\n", @@ -160,132 +152,140 @@ " Fusion Sites: ['C', 'D']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pJ23100_EB_E_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_EB/1\n", + " Name: DVA_DH_D_H\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_DH/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['E', 'B']\n", + " Fusion Sites: ['D', 'H']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pB0032_BC_B_C\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0032_BC/1\n", + " Name: pJ23116_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_EB/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['B', 'C']\n", + " Fusion Sites: ['E', 'B']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: DVA_FB_F_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_FB/1\n", + " Name: pJ23106_EB_E_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_EB/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['F', 'B']\n", + " Fusion Sites: ['E', 'B']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: DVA_DG_D_G\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_DG/1\n", + " Name: pJ23116_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_GB/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['D', 'G']\n", + " Fusion Sites: ['G', 'B']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pE0030_CD_C_D\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0030_CD/1\n", + " Name: pJ23116_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_AB/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['C', 'D']\n", + " Fusion Sites: ['A', 'B']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pJ23100_FB_F_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_FB/1\n", + " Name: DVA_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_BC/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['F', 'B']\n", + " Fusion Sites: ['B', 'C']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: DVA_DF_D_F\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_DF/1\n", + " Name: pB0033_BC_B_C\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0033_BC/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['D', 'F']\n", + " Fusion Sites: ['B', 'C']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pJ23106_EB_E_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_EB/1\n", + " Name: pJ23106_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_AB/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['E', 'B']\n", + " Fusion Sites: ['A', 'B']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pB0015_DF_D_F\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DF/1\n", + " Name: pB0015_DG_D_G\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DG/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['D', 'F']\n", + " Fusion Sites: ['D', 'G']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pJ23116_EB_E_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_EB/1\n", + " Name: pE0030_CD_C_D\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pE0030_CD/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['E', 'B']\n", + " Fusion Sites: ['C', 'D']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pJ23106_FB_F_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_FB/1\n", + " Name: DVA_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_FB/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", " Fusion Sites: ['F', 'B']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: DVA_BC_B_C\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_BC/1\n", + " Name: pJ23106_GB_G_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_GB/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['B', 'C']\n", + " Fusion Sites: ['G', 'B']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pB0034_BC_B_C\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0034_BC/1\n", + " Name: DVA_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_AB/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['B', 'C']\n", + " Fusion Sites: ['A', 'B']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pJ23116_AB_A_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23116_AB/1\n", + " Name: pJ23100_AB_A_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_AB/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", " Fusion Sites: ['A', 'B']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: pJ23106_GB_G_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_GB/1\n", + " Name: pJ23100_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23100_FB/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['G', 'B']\n", + " Fusion Sites: ['F', 'B']\n", " Antibiotic Resistance: Ampicillin\n", ", Plasmid:\n", - " Name: DVA_GB_G_B\n", - " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/DVA_GB/1\n", + " Name: pB0015_DF_D_F\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pB0015_DF/1\n", " Strain Definitions: [None]\n", " Plasmid Implementations: None\n", " Strain Implementations: None\n", - " Fusion Sites: ['G', 'B']\n", + " Fusion Sites: ['D', 'F']\n", + " Antibiotic Resistance: Ampicillin\n", + ", Plasmid:\n", + " Name: pJ23106_FB_F_B\n", + " Plasmid Definition: https://synbiohub.org/user/Gon/CIDARMoCloPlasmidsKit/pJ23106_FB/1\n", + " Strain Definitions: [None]\n", + " Plasmid Implementations: None\n", + " Strain Implementations: None\n", + " Fusion Sites: ['F', 'B']\n", " Antibiotic Resistance: Ampicillin\n", "]\n" ] @@ -305,7 +305,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 18, "id": "9cdd18d0", "metadata": {}, "outputs": [], @@ -321,7 +321,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 19, "id": "d76dbdeb", "metadata": {}, "outputs": [ @@ -331,24 +331,20 @@ "text": [ "amp_region\n", "B0033\n", + "dvk_backbone_core\n", "J23106\n", - "DVK_AE\n", "E0030_yfp\n", "kan_region\n", "E0040m_gfp\n", "B0015\n", "J23116\n", - "DVA_AF\n", "J23100\n", - "DVA_AF2\n", - "DVK_GH\n", "Fusion_Site_E\n", "Fusion_Site_F\n", "Fusion_Site_D\n", "Fusion_Site_G\n", "E1010m_rfp\n", "Fusion_Site_A\n", - "DVK_FG\n", "LacZ_cassette\n", "Fusion_Site_B\n", "Fusion_Site_C\n", @@ -358,8 +354,6 @@ "B0034\n", "dva_backbone_core\n", "origin_of_replication_pSB1A2\n", - "DVK_EF\n", - "dvk_backbone_core\n", "Design........................0\n", "Build.........................0\n", "Test..........................0\n", @@ -374,18 +368,21 @@ "Agent.........................0\n", "Attachment....................0\n", "CombinatorialDerivation.......0\n", - "Implementation................30\n", + "Implementation................36\n", "SampleRoster..................0\n", "Experiment....................0\n", "ExperimentalData..............0\n", "Annotation Objects............0\n", "---\n", - "Total: .........................30\n", + "Total: .........................36\n", "\n" ] } ], "source": [ + "from buildcompiler.constants import ENGINEERED_PLASMID, CIRCULAR, PLASMID_CLONING_VECTOR\n", + "\n", + "\n", "implementation_collection.default_namespace = (\n", " \"http://buildcompiler.org/implementations/\"\n", ")\n", @@ -395,7 +392,9 @@ "dummy_activity.types = \"http://sbols.org/v2#build\"\n", "\n", "for plasmid in plas_doc.componentDefinitions:\n", - " if \"http://identifiers.org/so/SO:0000637\" in plasmid.roles:\n", + " if (\n", + " ENGINEERED_PLASMID or PLASMID_CLONING_VECTOR in plasmid.roles\n", + " ) and CIRCULAR in plasmid.types:\n", " implementation = sbol2.Implementation(f\"{plasmid.displayId}_impl\")\n", " implementation.built = plasmid.identity\n", " implementation.wasGeneratedBy = dummy_activity\n", @@ -409,7 +408,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 20, "id": "3bcf02fb", "metadata": {}, "outputs": [ @@ -419,7 +418,7 @@ "'Valid.'" ] }, - "execution_count": 11, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -430,46 +429,52 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 21, "id": "9bf9c396", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "['pB0033_BC_impl',\n", - " 'DVA_CD_impl',\n", - " 'pJ23100_AB_impl',\n", - " 'pJ23106_AB_impl',\n", - " 'pJ23116_GB_impl',\n", - " 'DVA_DH_impl',\n", - " 'pB0015_DE_impl',\n", + "['pB0034_BC_impl',\n", " 'pJ23100_GB_impl',\n", + " 'DVA_DF_impl',\n", + " 'DVA_DG_impl',\n", + " 'DVA_AF_impl',\n", " 'pB0015_DH_impl',\n", - " 'DVA_AB_impl',\n", - " 'pJ23116_FB_impl',\n", - " 'pB0015_DG_impl',\n", + " 'pB0032_BC_impl',\n", " 'pE1010_CD_impl',\n", - " 'pE0040_CD_impl',\n", + " 'DVA_CD_impl',\n", + " 'DVA_GB_impl',\n", + " 'DVA_AF2_impl',\n", + " 'pJ23116_FB_impl',\n", + " 'pB0015_DE_impl',\n", " 'pJ23100_EB_impl',\n", - " 'pB0032_BC_impl',\n", - " 'DVA_FB_impl',\n", - " 'DVA_DG_impl',\n", - " 'pE0030_CD_impl',\n", - " 'pJ23100_FB_impl',\n", - " 'DVA_DF_impl',\n", - " 'pJ23106_EB_impl',\n", - " 'pB0015_DF_impl',\n", + " 'pE0040_CD_impl',\n", + " 'DVK_GH_impl',\n", + " 'DVA_DH_impl',\n", " 'pJ23116_EB_impl',\n", - " 'pJ23106_FB_impl',\n", - " 'DVA_BC_impl',\n", - " 'pB0034_BC_impl',\n", + " 'pJ23106_EB_impl',\n", + " 'pJ23116_GB_impl',\n", + " 'DVK_AE_impl',\n", " 'pJ23116_AB_impl',\n", + " 'DVA_BC_impl',\n", + " 'pB0033_BC_impl',\n", + " 'pJ23106_AB_impl',\n", + " 'pB0015_DG_impl',\n", + " 'pE0030_CD_impl',\n", + " 'DVK_EF_impl',\n", + " 'DVA_FB_impl',\n", " 'pJ23106_GB_impl',\n", - " 'DVA_GB_impl']" + " 'DVA_AB_impl',\n", + " 'DVK_FG_impl',\n", + " 'pJ23100_AB_impl',\n", + " 'pJ23100_FB_impl',\n", + " 'pB0015_DF_impl',\n", + " 'pJ23106_FB_impl']" ] }, - "execution_count": 12, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } From 223712359576ec74f5743289e2980b0f82e71d8d Mon Sep 17 00:00:00 2001 From: Ryan Greer Date: Mon, 18 May 2026 12:02:11 -0600 Subject: [PATCH 90/93] added transformation --- src/buildcompiler/buildcompiler.py | 181 ++++++++++++++++++++++++++++- 1 file changed, 180 insertions(+), 1 deletion(-) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index f345ba7..abc41f9 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -1,7 +1,7 @@ import sbol2 import random import warnings -from typing import List, Dict, Tuple +from typing import Any, List, Dict, Tuple from buildcompiler.plasmid import Plasmid from buildcompiler.sbol2build import ( @@ -530,6 +530,165 @@ def assembly_lvl2( return lvl2_plasmids, final_doc + def transformation( + self, + assembly_products: List[Plasmid], + chassis_name: str = "E_coli_DH5alpha", + transformation_doc: sbol2.Document = None, + ) -> Dict[str, Any]: + """Generate deterministic transformation artifacts from assembly outputs. + + :param assembly_products: Structured inputs produced by an assembly stage + :type assembly_products: List[Plasmid] + :param chassis_name: Display id used for the chassis module and implementation. + :type chassis_name: str + :param transformation_doc: Optional SBOL document to write outputs into. + :type transformation_doc: sbol2.Document | None + :returns: Structured transformation outputs including SBOL references, + robot JSON intermediate, protocol placeholders, and logs. + :rtype: dict + :raises ValueError: If no valid plasmid inputs can be extracted. + """ + if transformation_doc is None: + transformation_doc = self.sbol_doc + + chassis_module, chassis_impl = self._get_or_create_chassis( + transformation_doc, chassis_name + ) + + sbol_outputs = [] + robot_steps = [] + logs = [] + + for index, plasmid_obj in enumerate(assembly_products, start=1): + plasmid = plasmid_obj.plasmid_definition + + if not plasmid_obj.plasmid_implementations: + raise ValueError( + f"No plasmid implementations found for {plasmid.displayId}" + ) + + plasmid_impl = plasmid_obj.plasmid_implementations[0] + + transform_id = f"transform_{plasmid.displayId}_{index}" + + transformation_activity = sbol2.Activity(transform_id) + transformation_activity.name = ( + f"Transform {chassis_name} with {plasmid.displayId}" + ) + transformation_activity.types = "http://sbols.org/v2#build" + + chassis_usage = sbol2.Usage( + uri=f"{transform_id}_chassis", + entity=chassis_impl.identity, + role="http://sbols.org/v2#build", + ) + plasmid_usage = sbol2.Usage( + uri=f"{transform_id}_plasmid", + entity=plasmid_impl.identity, + role="http://sbols.org/v2#build", + ) + transformation_activity.usages = [chassis_usage, plasmid_usage] + + transformed_strain = sbol2.ModuleDefinition( + f"{chassis_name}_with_{plasmid.displayId}" + ) + transformed_strain.roles = [ORGANISM_STRAIN] + transformed_strain.name = ( + f"{chassis_name} transformed with {plasmid.displayId}" + ) + + chassis_module_ref = sbol2.Module( + uri=f"{transformed_strain.displayId}_chassis" + ) + chassis_module_ref.definition = chassis_module.identity + plasmid_fc = sbol2.FunctionalComponent( + uri=f"{transformed_strain.displayId}_plasmid" + ) + plasmid_fc.definition = plasmid.identity + + transformed_strain.modules = [chassis_module_ref] + transformed_strain.functionalComponents = [plasmid_fc] + + transformation_activity_association = sbol2.Association( + f"transform_{chassis_module_ref.name}" + ) + + transformation_activity_plan = sbol2.Plan( + f"{transformed_strain.displayId}_transformation_plan" + ) + transformation_activity_plan.description = ( + "TODO: generate accurate description of transformation" + ) + transformation_activity_association.plan = transformation_activity_plan + + transformation_activity_agent = sbol2.Agent("BuildCompiler") + transformation_activity_association.agent = transformation_activity_agent + + transformation_activity.associations = [transformation_activity_association] + + transformed_impl = sbol2.Implementation( + f"{transformed_strain.displayId}_impl" + ) + + transformed_impl.built = transformed_strain.identity + transformed_impl.wasGeneratedBy = transformation_activity.identity + + for obj in ( + transformation_activity, + chassis_usage, + plasmid_usage, + transformed_strain, + chassis_module_ref, + plasmid_fc, + transformed_impl, + ): + self._add_if_absent(transformation_doc, obj) + + sbol_outputs.append( + { + "transformation_activity": transformation_activity.identity, + "transformed_strain_module": transformed_strain.identity, + "transformed_strain_implementation": transformed_impl.identity, + } + ) + robot_steps.append( + { + "step": index, + "plasmid": plasmid.displayId, + "chassis": chassis_name, + "mix_ul": {"competent_cells": 50, "assembly_product": 5}, + "heat_shock": {"temperature_c": 42, "duration_seconds": 45}, + "recovery": {"medium": "SOC", "volume_ul": 950, "duration_min": 60}, + } + ) + logs.append( + f"Prepared transformation input for plasmid {plasmid.displayId} into chassis {chassis_name}." + ) + + return { + "stage": "transformation", + "inputs": [ + plasmid.plasmid_definition.displayId for plasmid in assembly_products + ], + "chassis": chassis_name, + "sbol_artifacts": sbol_outputs, + "json_intermediate": { + "protocol": "chemical_transformation", + "version": "0.1", + "steps": robot_steps, + }, + "protocol_artifacts": { + "ot2_script": "TODO: adapter to protocol generator", + "human_instructions": [ + "Thaw competent cells on ice.", + "Combine assembly product with competent cells as specified.", + "Run heat shock and recovery according to generated parameters.", + ], + "logs": logs, + }, + } + def _extract_plasmids_from_strain( self, strain: sbol2.ModuleDefinition, @@ -1106,6 +1265,26 @@ def _create_ligase_implementation(self): self.sbol_doc.add_list([T4_impl, ligase_def]) self.T4_ligase_impl = T4_impl + def _add_if_absent(self, doc: sbol2.Document, obj: Any): + if doc.find(obj.identity) is None: + doc.add(obj) + + def _get_or_create_chassis( + self, doc: sbol2.Document, chassis_name: str + ) -> tuple[sbol2.ModuleDefinition, sbol2.Implementation]: + chassis_module = doc.find(chassis_name) or sbol2.ModuleDefinition(chassis_name) + chassis_module.roles = [ORGANISM_STRAIN] + chassis_module.name = chassis_name + self._add_if_absent(doc, chassis_module) + + chassis_impl_id = f"{chassis_name}_impl" + chassis_impl = doc.find(chassis_impl_id) or sbol2.Implementation( + chassis_impl_id + ) + chassis_impl.built = chassis_module.identity + self._add_if_absent(doc, chassis_impl) + return chassis_module, chassis_impl + def _extract_lvl2_TUs( # TODO send to misc helper file instead of buildcompiler.py? design_doc: sbol2.Document, From 2ed6001e4b1ae9306dc455b135370a029bf0bba2 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:20:40 -0600 Subject: [PATCH 91/93] Fix domestication BsaI implementation reference --- src/buildcompiler/buildcompiler.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index e3f6b80..46a3a68 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -11,13 +11,11 @@ from buildcompiler.plasmid import Plasmid from buildcompiler.sbol2build import ( - Assembly, assembly_lvl2, - dna_componentdefinition_with_sequence, - rebase_restriction_enzyme, -) -from sbol2build.abstract_translator import enumerate_design_variants, + Assembly, Transformation as SBOL2Transformation, + assembly_lvl2, dna_componentdefinition_with_sequence, + rebase_restriction_enzyme, ) from .abstract_translator import ( enumerate_design_variants, @@ -362,7 +360,7 @@ def _remove_internal_bsai_sites(sequence: str) -> tuple[str, int]: assembly = Assembly( [Plasmid(insert_definition, None, [insert_impl], [], self.sbol_doc)], backbone, - self.bsaI_impl, + self.BsaI_impl, self.T4_ligase_impl, self.sbol_doc, ) From f10a5b48254f0bc37dc8e30fcd51c4d5d0a3a465 Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:34:21 -0600 Subject: [PATCH 92/93] Respect supplied level 2 backbone --- src/buildcompiler/buildcompiler.py | 39 +++++++++------ tests/unit/stages/test_assembly_lvl2.py | 64 ++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 16 deletions(-) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index 46a3a68..99cd1ce 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -13,7 +13,6 @@ from buildcompiler.sbol2build import ( Assembly, Transformation as SBOL2Transformation, - assembly_lvl2, dna_componentdefinition_with_sequence, rebase_restriction_enzyme, ) @@ -372,8 +371,9 @@ def _remove_internal_bsai_sites(sequence: str) -> tuple[str, int]: def assembly_lvl1( self, - abstract_designs: List[sbol2.ComponentDefinition] - | sbol2.CombinatorialDerivation, + abstract_designs: ( + List[sbol2.ComponentDefinition] | sbol2.CombinatorialDerivation + ), final_doc: sbol2.Document = sbol2.Document(), product_name: str = "composite", backbone: Plasmid | Dict[str, Plasmid] | None = None, @@ -538,14 +538,14 @@ def assembly_lvl2( # l1 backbone zselection backbone_fusion_sites = LVL2_FUSION_SITE_ORDER[i] - backbone = next( + lvl1_backbone = next( plasmid for plasmid in self.indexed_backbones if plasmid.fusion_sites == backbone_fusion_sites and plasmid.antibiotic_resistance == KAN ) - backbone_dict[TU.displayId] = backbone + backbone_dict[TU.displayId] = lvl1_backbone # TODO insert check here to see if the TU exists already (#43). should not be too expensive, as long as we search only indexed_plasmids where AR=KAN @@ -565,7 +565,8 @@ def assembly_lvl2( key = p.plasmid_definition.displayId plasmid_dict.setdefault(key, []).append(p) - backbone, _ = self._get_backbone(plasmid_dict, antibiotic_resistance=AMP) + if backbone is None: + backbone, _ = self._get_backbone(plasmid_dict, antibiotic_resistance=AMP) print(backbone) @@ -795,7 +796,9 @@ def transformation( ) normalized_plasmids = [] for product in normalized_products: - indexed = self._get_indexed_plasmid(self.indexed_plasmids, product["plasmid"]) + indexed = self._get_indexed_plasmid( + self.indexed_plasmids, product["plasmid"] + ) if indexed is None: indexed = type( "TransformationPlasmid", @@ -874,9 +877,7 @@ def plating( advanced_params = advanced_params or {} doc_ref = plating_doc or self.sbol_doc - normalized = normalize_plating_input( - transformation_results, doc=doc_ref - ) + normalized = normalize_plating_input(transformation_results, doc=doc_ref) if len(normalized) > 96: raise ValueError("plating supports up to 96 transformed strains.") @@ -918,7 +919,10 @@ def plating( plate_layout_csv = results_path / "plate_layout_dataframe.csv" with plate_layout_csv.open("w", newline="", encoding="utf-8") as handle: - writer = csv.DictWriter(handle, fieldnames=list(plate_rows[0].keys()) if plate_rows else ["well"]) + writer = csv.DictWriter( + handle, + fieldnames=list(plate_rows[0].keys()) if plate_rows else ["well"], + ) writer.writeheader() for row in plate_rows: writer.writerow(row) @@ -931,7 +935,9 @@ def plating( "well_map": plate_rows, }, ) - plate_map_csv_path = write_plate_map_csv(results_path / "plate_map.csv", plate_rows) + plate_map_csv_path = write_plate_map_csv( + results_path / "plate_map.csv", plate_rows + ) plating_input_json_path = write_plate_map_json( results_path / "plating_input.json", {"bacterium_locations": bacterium_locations}, @@ -984,7 +990,9 @@ def plating( }, "metadata": { "plate_rows": plate_rows, - "layout_dataframe_columns": list(plate_rows[0].keys()) if plate_rows else [], + "layout_dataframe_columns": ( + list(plate_rows[0].keys()) if plate_rows else [] + ), }, "json_intermediate": { "plating_data": {"bacterium_locations": bacterium_locations}, @@ -1073,8 +1081,9 @@ def _sort_plasmid_components( self, definition: sbol2.ComponentDefinition, doc: sbol2.Document ): if len(definition.components) > 1: - if ENGINEERED_PLASMID in definition.roles and not self._get_indexed_plasmid( - self.indexed_plasmids, definition + if ( + ENGINEERED_PLASMID in definition.roles + and not self._get_indexed_plasmid(self.indexed_plasmids, definition) ): self.indexed_plasmids.append(Plasmid(definition, None, [], [], doc)) elif ( diff --git a/tests/unit/stages/test_assembly_lvl2.py b/tests/unit/stages/test_assembly_lvl2.py index 17466c7..61d3af7 100644 --- a/tests/unit/stages/test_assembly_lvl2.py +++ b/tests/unit/stages/test_assembly_lvl2.py @@ -205,4 +205,66 @@ def test_assembly_lvl2_incomplete_region_order_falls_back_with_warning(): assert result.status == StageStatus.SUCCESS assert result.protocol_artifacts["selected_route"] is not None - assert any("Unable to satisfy region_order constraint" in log for log in result.logs) + assert any( + "Unable to satisfy region_order constraint" in log for log in result.logs + ) + + +def test_buildcompiler_assembly_lvl2_respects_supplied_backbone(monkeypatch): + from types import SimpleNamespace + + from buildcompiler.buildcompiler import BuildCompiler + import buildcompiler.buildcompiler as buildcompiler_module + + compiler = BuildCompiler.from_local_documents([]) + compiler.BbsI_impl = object() + compiler.T4_ligase_impl = object() + + lvl1_backbone = SimpleNamespace( + fusion_sites=buildcompiler_module.LVL2_FUSION_SITE_ORDER[0], + antibiotic_resistance=buildcompiler_module.KAN, + ) + supplied_lvl2_backbone = SimpleNamespace(name="caller-supplied-lvl2-backbone") + compiler.indexed_backbones = [lvl1_backbone] + + tu = SimpleNamespace(displayId="TU1") + lvl1_plasmid = SimpleNamespace( + plasmid_definition=SimpleNamespace(displayId="TU1_plasmid") + ) + captured = {} + + class FakeAssembly: + def __init__(self, plasmids, backbone, *args): + captured["plasmids"] = plasmids + captured["backbone"] = backbone + + def run(self): + return [ + SimpleNamespace( + plasmid_definition=SimpleNamespace(displayId="lvl2_product") + ) + ], sbol2.Document() + + def fail_get_backbone(*args, **kwargs): + raise AssertionError( + "assembly_lvl2 should not auto-select a backbone when one is supplied" + ) + + monkeypatch.setattr(buildcompiler_module, "_extract_lvl2_TUs", lambda doc: [tu]) + monkeypatch.setattr(buildcompiler_module, "Assembly", FakeAssembly) + monkeypatch.setattr( + compiler, + "assembly_lvl1", + lambda *args, **kwargs: ({"TU1": [object()]}, sbol2.Document()), + ) + monkeypatch.setattr( + compiler, "_encapsulate_TU", lambda composite: (lvl1_plasmid, []) + ) + monkeypatch.setattr(compiler, "_get_backbone", fail_get_backbone) + + products, _ = compiler.assembly_lvl2( + sbol2.Document(), backbone=supplied_lvl2_backbone, product_name="lvl2" + ) + + assert products + assert captured["backbone"] is supplied_lvl2_backbone From de4387a2ae422fbf93fea438cc71e5f7ca69ab7a Mon Sep 17 00:00:00 2001 From: Gonzalo Vidal <35148159+Gonza10V@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:45:44 -0600 Subject: [PATCH 93/93] Select top-level design for lvl2 TU extraction --- src/buildcompiler/buildcompiler.py | 3 ++- tests/unit/stages/test_assembly_lvl2.py | 27 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/buildcompiler/buildcompiler.py b/src/buildcompiler/buildcompiler.py index 99cd1ce..e1de21e 100644 --- a/src/buildcompiler/buildcompiler.py +++ b/src/buildcompiler/buildcompiler.py @@ -19,6 +19,7 @@ from .abstract_translator import ( enumerate_design_variants, extract_combinatorial_design_parts, + extract_toplevel_definition, get_or_pull, get_compatible_plasmids, ) @@ -1612,7 +1613,7 @@ def _extract_lvl2_TUs( # TODO send to misc helper file instead of buildcompiler Returns: A list of TU component definitions in sequential order. """ - top_design = design_doc.componentDefinitions[0] + top_design = extract_toplevel_definition(design_doc) return [ design_doc.get(comp.definition) for comp in top_design.getInSequentialOrder() diff --git a/tests/unit/stages/test_assembly_lvl2.py b/tests/unit/stages/test_assembly_lvl2.py index 61d3af7..b0c7206 100644 --- a/tests/unit/stages/test_assembly_lvl2.py +++ b/tests/unit/stages/test_assembly_lvl2.py @@ -268,3 +268,30 @@ def fail_get_backbone(*args, **kwargs): assert products assert captured["backbone"] is supplied_lvl2_backbone + + +def test_extract_lvl2_tus_uses_top_level_composite_not_document_order(): + from buildcompiler.buildcompiler import _extract_lvl2_TUs + + doc = sbol2.Document() + tu1 = sbol2.ComponentDefinition("tu1", sbol2.BIOPAX_DNA) + tu2 = sbol2.ComponentDefinition("tu2", sbol2.BIOPAX_DNA) + composite = sbol2.ComponentDefinition("lvl2_design", sbol2.BIOPAX_DNA) + + # Add child TUs before the composite to ensure extraction does not depend on + # ComponentDefinition serialization/document order. + doc.addComponentDefinition(tu1) + doc.addComponentDefinition(tu2) + doc.addComponentDefinition(composite) + + tu1_component = composite.components.create("tu1_component") + tu1_component.definition = tu1.identity + tu2_component = composite.components.create("tu2_component") + tu2_component.definition = tu2.identity + + tu_order = composite.sequenceConstraints.create("tu1_before_tu2") + tu_order.subject = tu1_component.identity + tu_order.object = tu2_component.identity + tu_order.restriction = sbol2.SBOL_RESTRICTION_PRECEDES + + assert _extract_lvl2_TUs(doc) == [tu1, tu2]