-
Notifications
You must be signed in to change notification settings - Fork 1
Implement transformation stage in BuildCompiler orchestrator #45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| from .sbol2build import * # noqa: F403 | ||
| from .buildcompiler import BuildCompiler as BuildCompiler |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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,166 @@ def assembly_lvl2( | |
|
|
||
| return protocol | ||
|
|
||
| def transformation( | ||
| self, | ||
| assembly_products: List[Plasmid] | None = None, | ||
| plasmid_inputs: List[sbol2.ComponentDefinition] | None = None, | ||
| chassis_name: str = "E_coli_DH5alpha", | ||
| ) -> Dict[str, Any]: | ||
| """Generate a deterministic transformation plan for one or more plasmids. | ||
|
|
||
| The method accepts either assembled :class:`Plasmid` objects (preferred) or | ||
| standalone plasmid ``ComponentDefinition`` inputs and returns structured | ||
| transformation artifacts for downstream orchestration. | ||
|
|
||
| :param assembly_products: Assembled plasmid objects from an upstream assembly stage. | ||
| :param plasmid_inputs: Plasmid definitions when assembly outputs are unavailable. | ||
| :param chassis_name: Label used to name chassis/transformed-strain artifacts. | ||
| :raises ValueError: If neither, or both, plasmid input channels are supplied. | ||
| :returns: Structured transformation output including SBOL references, JSON plan, | ||
| and protocol placeholders. | ||
| :rtype: Dict[str, Any] | ||
| """ | ||
| plasmids = self._resolve_transformation_inputs(assembly_products, plasmid_inputs) | ||
|
|
||
| transformation_md = sbol2.ModuleDefinition(f"{chassis_name}_transformation") | ||
| transformation_md.name = f"{chassis_name} chemical transformation" | ||
| self.sbol_doc.add(transformation_md) | ||
|
|
||
| transformed_strains: List[Dict[str, str]] = [] | ||
| transformation_records: List[Dict[str, Any]] = [] | ||
|
|
||
| for index, plasmid in enumerate(plasmids, start=1): | ||
| plasmid_identity = plasmid.plasmid_definition.identity | ||
| plasmid_display_id = plasmid.plasmid_definition.displayId | ||
|
|
||
| activity = sbol2.Activity( | ||
| f"{chassis_name}_transform_{plasmid_display_id}_{index}" | ||
| ) | ||
| activity.name = f"Chemical transformation of {plasmid_display_id}" | ||
| activity.types = ["http://sbols.org/v2#build"] | ||
| self.sbol_doc.add(activity) | ||
|
|
||
| transformed_strain = sbol2.ModuleDefinition( | ||
| f"{chassis_name}_with_{plasmid_display_id}_{index}" | ||
| ) | ||
| transformed_strain.name = ( | ||
| f"{chassis_name} transformed with {plasmid_display_id}" | ||
| ) | ||
| transformed_strain.roles = [ORGANISM_STRAIN] | ||
|
|
||
| plasmid_fc = transformed_strain.functionalComponents.create( | ||
| f"{plasmid_display_id}_engineered_plasmid" | ||
| ) | ||
| plasmid_fc.definition = plasmid_identity | ||
|
|
||
| transformed_impl = sbol2.Implementation( | ||
| f"{transformed_strain.displayId}_impl" | ||
| ) | ||
| transformed_impl.built = transformed_strain.identity | ||
| transformed_impl.wasGeneratedBy = activity.identity | ||
|
|
||
| self.sbol_doc.add_list([transformed_strain, transformed_impl]) | ||
|
|
||
| transformation_records.append( | ||
| { | ||
| "reaction_id": f"transform_{index}", | ||
| "plasmid": plasmid_display_id, | ||
| "plasmid_identity": plasmid_identity, | ||
| "destination_strain": transformed_strain.displayId, | ||
| "total_volume_ul": 50, | ||
| "plasmid_volume_ul": 2, | ||
| "competent_cells_volume_ul": 48, | ||
| "outgrowth_minutes": 60, | ||
| } | ||
| ) | ||
|
|
||
| transformed_strains.append( | ||
| { | ||
| "module_definition": transformed_strain.identity, | ||
| "implementation": transformed_impl.identity, | ||
| "activity": activity.identity, | ||
| } | ||
| ) | ||
|
|
||
| robot_json = { | ||
| "stage": "transformation", | ||
| "protocol": "chemical_transformation", | ||
| "chassis": chassis_name, | ||
| "reactions": transformation_records, | ||
| } | ||
| protocol_bundle = self._build_transformation_protocol_bundle( | ||
| chassis_name=chassis_name, reactions=transformation_records | ||
| ) | ||
|
|
||
| return { | ||
| "stage": "transformation", | ||
| "inputs": [plasmid.plasmid_definition.identity for plasmid in plasmids], | ||
| "sbol": { | ||
| "transformation_module": transformation_md.identity, | ||
| "transformed_strains": transformed_strains, | ||
| }, | ||
| "json": robot_json, | ||
| "artifacts": protocol_bundle, | ||
| } | ||
|
|
||
| def _resolve_transformation_inputs( | ||
| self, | ||
| assembly_products: List[Plasmid] | None, | ||
| plasmid_inputs: List[sbol2.ComponentDefinition] | None, | ||
| ) -> List[Plasmid]: | ||
| if bool(assembly_products) == bool(plasmid_inputs): | ||
| raise ValueError( | ||
| "Provide either assembly_products or plasmid_inputs (exactly one)." | ||
| ) | ||
|
|
||
| if assembly_products: | ||
| return assembly_products | ||
|
|
||
| resolved_plasmids: List[Plasmid] = [] | ||
| for plasmid_definition in plasmid_inputs or []: | ||
| existing_plasmid = self._get_indexed_plasmid( | ||
| self.indexed_plasmids, plasmid_definition | ||
| ) | ||
| if existing_plasmid: | ||
| resolved_plasmids.append(existing_plasmid) | ||
| continue | ||
|
|
||
| generated_impl = sbol2.Implementation(f"{plasmid_definition.displayId}_impl") | ||
| generated_impl.built = plasmid_definition.identity | ||
| self.sbol_doc.add(generated_impl) | ||
|
Comment on lines
+491
to
+493
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For Useful? React with 👍 / 👎. |
||
|
|
||
| resolved_plasmids.append( | ||
| Plasmid( | ||
| plasmid_definition, | ||
| None, | ||
| [generated_impl], | ||
| [], | ||
| self.sbol_doc, | ||
| ) | ||
| ) | ||
|
|
||
| return resolved_plasmids | ||
|
|
||
| def _build_transformation_protocol_bundle( | ||
| self, chassis_name: str, reactions: List[Dict[str, Any]] | ||
| ) -> Dict[str, Any]: | ||
| instruction_lines = [ | ||
| f"1. Thaw competent {chassis_name} cells on ice.", | ||
| "2. Add 48 µL competent cells + 2 µL assembly product for each reaction.", | ||
| "3. Incubate on ice for 30 minutes.", | ||
| "4. Heat shock at 42°C for 45 seconds, then return to ice for 2 minutes.", | ||
| "5. Add 450 µL SOC media and recover at 37°C for 60 minutes.", | ||
| ] | ||
| return { | ||
| "protocol_name": "chemical_transformation", | ||
| "opentrons_template": "ot2_chemical_transformation_v1", | ||
| "instructions": instruction_lines, | ||
| "log": [ | ||
| f"Prepared {len(reactions)} transformation reaction(s) for {chassis_name}." | ||
| ], | ||
| } | ||
|
|
||
| def _extract_plasmids_from_strain( | ||
| self, | ||
| strain: sbol2.ModuleDefinition, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| 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 | ||
| from buildcompiler.plasmid import 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 _build_plasmid(self, display_id: str) -> Plasmid: | ||
| plasmid_definition = sbol2.ComponentDefinition(display_id) | ||
| plasmid_definition.roles = [ENGINEERED_PLASMID] | ||
| self.doc.add(plasmid_definition) | ||
|
|
||
| plasmid_impl = sbol2.Implementation(f"{display_id}_impl") | ||
| plasmid_impl.built = plasmid_definition.identity | ||
| self.doc.add(plasmid_impl) | ||
|
|
||
| return Plasmid(plasmid_definition, None, [plasmid_impl], [], self.doc) | ||
|
|
||
| def test_transformation_with_assembly_products(self): | ||
| plasmid = self._build_plasmid("assembled_gene") | ||
|
|
||
| result = self.compiler.transformation(assembly_products=[plasmid]) | ||
|
|
||
| self.assertEqual(result["stage"], "transformation") | ||
| self.assertEqual(len(result["json"]["reactions"]), 1) | ||
| self.assertEqual(result["json"]["reactions"][0]["plasmid"], "assembled_gene") | ||
| self.assertEqual(len(result["sbol"]["transformed_strains"]), 1) | ||
|
|
||
| def test_transformation_with_plasmid_inputs(self): | ||
| plasmid_definition = sbol2.ComponentDefinition("input_plasmid") | ||
| plasmid_definition.roles = [ENGINEERED_PLASMID] | ||
| self.doc.add(plasmid_definition) | ||
|
|
||
| result = self.compiler.transformation(plasmid_inputs=[plasmid_definition]) | ||
|
|
||
| self.assertEqual(result["stage"], "transformation") | ||
| self.assertEqual(result["inputs"], [plasmid_definition.identity]) | ||
| self.assertEqual(result["json"]["reactions"][0]["destination_strain"], "E_coli_DH5alpha_with_input_plasmid_1") | ||
|
|
||
| def test_transformation_requires_single_input_channel(self): | ||
| with self.assertRaises(ValueError): | ||
| self.compiler.transformation() | ||
|
|
||
| plasmid = self._build_plasmid("dual_mode") | ||
| with self.assertRaises(ValueError): | ||
| self.compiler.transformation( | ||
| assembly_products=[plasmid], | ||
| plasmid_inputs=[plasmid.plasmid_definition], | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This module ID is derived only from
chassis_name, so any second call toBuildCompiler.transformation(...)with the same chassis on the samesbol_docwill attempt to add the sameModuleDefinitionidentity again and fail due to duplicate SBOL identities. That blocks the documented multi-stage workflow where transformation is run more than once (e.g., after different assembly stages) unless callers manually changechassis_nameeach time.Useful? React with 👍 / 👎.