diff --git a/sbol_utilities/build_planning.py b/sbol_utilities/build_planning.py new file mode 100644 index 00000000..8412efe1 --- /dev/null +++ b/sbol_utilities/build_planning.py @@ -0,0 +1,135 @@ +import sbol3 +import tyto + +from sbol_utilities.component import get_subcomponents, get_subcomponents_by_identity +from sbol_utilities.helper_functions import is_plasmid + +# TODO: Delete SBOL_ASSEMBLY_PLAN and change its references to tyto.SBOL3.assemblyPlan once tyto supports the +# Design-Build-Test-Learn portion of the SBOL3 ontology +# See issues https://github.com/SynBioDex/tyto/issues/56 and https://github.com/SynBioDex/sbol-owl3/issues/5 +SBOL_ASSEMBLY_PLAN = 'http://sbols.org/v3#assemblyPlan' +ASSEMBLY_TYPES = {sbol3.SBOL_DESIGN, SBOL_ASSEMBLY_PLAN} + + +def validate_part_in_backbone(pib: sbol3.Component) -> bool: + """Check if a Component represents a part in backbone + + :param plan: Component being validated + :return: true if it has SubComponents for one insert and one vector backbone + """ + subcomps = get_subcomponents(pib) + + comps = [sc.instance_of.lookup() for sc in subcomps] + + # Get Components for SubComponents of part in backbone that have engineered_insert as one of their roles + # (that is, a role of either the Component or its SubComponent instance) + inserts = [comps[i] for i in range(0, len(comps)) + if tyto.SO.engineered_insert in subcomps[i].roles or tyto.SO.engineered_insert in comps[i].roles] + + # Get Components for SubComponents of part in backbone that are plasmids according to their roles + # (that is, the roles of the Components) + backbones = [c for c in comps if is_plasmid(c)] + + return len(inserts) == 1 and len(backbones) == 1 + + +def validate_composite_part_assemblies(c: sbol3.Component) -> bool: + """Check if a Component for a composite part has only valid assemblies + + :param plan: Component being validated + :return: true if all of its assemblies are valid (see validate_assembly) + """ + activities = [g.lookup() for g in c.generated_by] + + invalid_assemblies = [a for a in activities if is_assembly(a) and not validate_assembly(a, c)] + + return len(invalid_assemblies) == 0 + + +def validate_assembly_component(ac: sbol3.Component, composite_part: sbol3.Component) -> bool: + """Check if Component represents the assembly of a composite part + + :param plan: Component being validated and Component for composite part + :return: true if it has (1) SubComponents for the composite part and its assembled parts + (2) SubComponents for these parts in their backbones, and + (3) a contains Constraint for each part in backbone and its insert. + """ + # Get identities of Components that are SubComponents of the assembly Component + assembly_subcomps = get_subcomponents(ac) + assembly_ids = {str(sc.instance_of) for sc in assembly_subcomps} + + # Get identities of Components for assembled parts that are SubComponents of the composite part + assembled_subcomps = get_subcomponents(composite_part) + assembled_ids = {str(sc.instance_of) for sc in assembled_subcomps} + + # Check whether composite part is SubComponent of the assembly Component + has_composite = composite_part.identity in assembly_ids + + # Determine identities of Components for assembled parts that are not SubComponents of the assembly Component + unassembled = assembled_ids.difference(assembly_ids) + + # Get identities of SubComponents for composite part and assembled parts in the assembly Component + for assembly_subcomponent in assembly_subcomps: + if str(assembly_subcomponent.instance_of) == composite_part.identity: + composite_subid = assembly_subcomponent.identity + + assembled_subids = {sc.identity for sc in assembly_subcomps if str(sc.instance_of) in assembled_ids} + + # Build map from object to subject SubComponent identities for all contains Constraints in the assembly Component + # TODO: Change sbol3.SBOL_CONTAINS to tyto.SBOL3.contains once tyto supports SBOL3 constraint restrictions + # See issues https://github.com/SynBioDex/tyto/issues/55 and https://github.com/SynBioDex/sbol-owl3/issues/4 + contained_map = {str(co.object) : str(co.subject) for co in ac.constraints if co.restriction == sbol3.SBOL_CONTAINS} + + # Determine identities of SubComponents for assembly parts that are not the object of a contains Constraint + uncontained = assembled_subids.difference(contained_map.keys()) + + # Add identity of SubComoponent for composite part to uncontained set if it is not the object of contains Constraint + if has_composite: + if composite_subid not in contained_map.keys(): + uncontained.add(composite_subid) + + + # Get identities of SubComponents for parts in backbones that contain an assembled part or composite part + pib_subids = [contained_map[key] for key in contained_map.keys() + if key in assembled_subids or (has_composite and key == composite_subid)] + + # Get identities of Components for parts in backbones + parts_in_backbones = [sc.instance_of.lookup() for sc in get_subcomponents_by_identity(ac, pib_subids)] + + # Determine which part in backbone Components are invalid + invalid_parts_in_backbones = [pib for pib in parts_in_backbones if not validate_part_in_backbone(pib)] + + # ligations = [i for i in assembly_comps[0].interactions if tyto.SBO.conversion in i.types] + + # digestions = [i for i in assembly_comps[0].interactions if tyto.SBO.cleavage in i.types] + + return (len(unassembled) == 0 and len(uncontained) == 0 and has_composite + and len(invalid_parts_in_backbones) == 0) + + +def is_assembly(a: sbol3.Activity) -> bool: + """Check if Activity is an assembly + + :param plan: Activity being checked + :return: true if it has the expected types for an assembly + """ + return set(ASSEMBLY_TYPES).issubset(a.types) + + +def validate_assembly(a: sbol3.Activity, composite_part: sbol3.Component) -> bool: + """Check if Activity represents the assembly of a composite part + + :param plan: Activity being validated and Component for composite part + :return: true if it uses a single valid assembly Component (see validate_assembly_component) + """ + # TODO: Change sbol3.SBOL_Design to tyto.SBOL3.design once tyto supports the + # Design-Build-Test-Learn portion of the SBOL3 ontology + # See issues https://github.com/SynBioDex/tyto/issues/56 and https://github.com/SynBioDex/sbol-owl3/issues/5 + assembly_comps = [a.document.find(u.entity) for u in a.usage if sbol3.SBOL_DESIGN in u.roles] + + is_assembly_comp_valid = True + for assembly_comp in assembly_comps: + if not validate_assembly_component(assembly_comp, composite_part): + is_assembly_comp_valid = False + + return len(assembly_comps) == 1 and is_assembly_comp_valid diff --git a/sbol_utilities/component.py b/sbol_utilities/component.py index 508d2990..cd78c86f 100644 --- a/sbol_utilities/component.py +++ b/sbol_utilities/component.py @@ -11,6 +11,24 @@ from sbol_utilities.workarounds import get_parent +def get_subcomponents(c: sbol3.Component) -> List[sbol3.SubComponent]: + """Get all Features of Component that are SubComponents + + :param obj: Component to get Features from + :return: List of Component Features that are SubComponents + """ + return [f for f in c.features if isinstance(f, sbol3.SubComponent)] + + +def get_subcomponents_by_identity(c: sbol3.Component, ids: List[str]) -> List[sbol3.SubComponent]: + """Get all SubComponents of Component that are instances of Components identified in ids + + :param obj: Component to get Subcomponents from + :return: List of SubComponents that are instances of Components identified in ids + """ + return [sc for sc in get_subcomponents(c) if sc.identity in ids] + + # TODO: consider allowing return of LocalSubComponent and ExternallyDefined def contained_components(roots: Union[sbol3.TopLevel, Iterable[sbol3.TopLevel]]) -> Set[sbol3.Component]: """Find the set of all SBOL Components contained within the roots or their children. diff --git a/test/test_build_planning.py b/test/test_build_planning.py new file mode 100644 index 00000000..35af3fcb --- /dev/null +++ b/test/test_build_planning.py @@ -0,0 +1,169 @@ +import unittest + +import sbol3 +import tyto +from typing import Optional + +from sbol_utilities.build_planning import validate_composite_part_assemblies, SBOL_ASSEMBLY_PLAN + + +class TestBuildPlanning(unittest.TestCase): + + def test_validate_composite_part_assemblies(self): + """Test function for validating composite part assemblies""" + test_doc = sbol3.Document() + + sbol3.set_namespace('http://testBuildPlanning.org') + + # Should work since composite part BBa_K093005 has an assembly Activity that uses an assembly Component + # that has SubComponents for BBa_K093005 and its assembled parts BBa_B0034 and BBa_E1010 and their parts in + # backbones. The assembly Component also has a contains Constraint with each part in backbone as a subject + # and the part insert as an object. + assert validate_composite_part_assemblies(assemble_BBa_K093005(test_doc)) + + # Should not work since the assembly Component is missing a SubComponent for the assembled part BBa_E1010 + assert not validate_composite_part_assemblies(assemble_BBa_K093005(test_doc, 'BBa_E1010_UNASSEMBLED')) + + # Should not work since the assembly Component is missing a SubComponent for the composite part BBa_K093005 + assert not validate_composite_part_assemblies(assemble_BBa_K093005(test_doc, 'BBa_K093005_UNASSEMBLED')) + + # Should not work since the assembly Component has no Constraint with BBa_E1010 as its object and its part in + # backbone as its subject + assert not validate_composite_part_assemblies(assemble_BBa_K093005(test_doc, 'BBa_E1010_UNCONTAINED')) + + # Should not work since the part in backbone Component for BBa_E1010 is missing its insert SubComponent + assert not validate_composite_part_assemblies(assemble_BBa_K093005(test_doc, 'BBa_E1010_NOT_INSERT')) + + # Should not work since the part in backbone Component for BBa_E1010 is missing its backbone SubComponent + assert not validate_composite_part_assemblies(assemble_BBa_K093005(test_doc, 'pSB1C3_NOT_BACKBONE')) + + # Should not work since the assembly Activity uses more than one assembly Component + assert not validate_composite_part_assemblies(assemble_BBa_K093005(test_doc, 'EXTRA_ASSEMBLY_COMPONENT')) + +def assemble_BBa_K093005(doc: sbol3.Document, failure_mode: Optional[str] = ''): + doc = sbol3.Document() + + sbol3.set_namespace('http://test_build_planning.org') + + # Create assembled parts BBa_B0034 and BBa_E1010 + + assembled_rbs = sbol3.Component('BBa_B0034', sbol3.SBO_DNA, roles=[tyto.SO.ribosome_entry_site]) + assembled_cds = sbol3.Component('BBa_E1010', sbol3.SBO_DNA, roles=[tyto.SO.CDS]) + + doc.add(assembled_rbs) + doc.add(assembled_cds) + + # Create composite part BBa_K093005 + + composite_part = sbol3.Component('BBa_K093005', sbol3.SBO_DNA, roles=[tyto.SO.engineered_region]) + + composite_part_sc1 = sbol3.SubComponent(assembled_rbs) + composite_part_sc2 = sbol3.SubComponent(assembled_cds) + + composite_part.features += [composite_part_sc1] + composite_part.features += [composite_part_sc2] + + doc.add(composite_part) + + # Create backbone pSB1C3 + + backbone = sbol3.Component('pSB1C3', [sbol3.SBO_DNA, tyto.SO.circular], + roles=[tyto.SO.plasmid_vector]) + + doc.add(backbone) + + # Create part in backbone for BBa_B0034 in pSB1C3 + + rbs_in_backbone = sbol3.Component('BBa_B0034_in_pSB1C3', [sbol3.SBO_DNA, tyto.SO.circular], + roles=[tyto.SO.plasmid_vector]) + + pib1_sc1 = sbol3.SubComponent(assembled_rbs, roles=[tyto.SO.engineered_insert]) + rbs_in_backbone.features += [pib1_sc1] + pib1_sc2 = sbol3.SubComponent(backbone) + rbs_in_backbone.features += [pib1_sc2] + + doc.add(rbs_in_backbone) + + # Create part in backbone for BBa_E1010 in pSB1C3 + + cds_in_backbone = sbol3.Component('BBa_E1010_in_pSB1C3', [sbol3.SBO_DNA, tyto.SO.circular], + roles=[tyto.SO.plasmid_vector]) + + if failure_mode != 'BBa_E1010_NOT_INSERT': + pib2_sc1 = sbol3.SubComponent(assembled_cds, roles=[tyto.SO.engineered_insert]) + cds_in_backbone.features += [pib2_sc1] + + if failure_mode != 'pSB1C3_NOT_BACKBONE': + pib2_sc2 = sbol3.SubComponent(backbone) + cds_in_backbone.features += [pib2_sc2] + + doc.add(cds_in_backbone) + + # Create part in backbone for BBa_K093005 in pSB1C3 + + gene_in_backbone = sbol3.Component('BBa_K093005_in_pSB1C3', [sbol3.SBO_DNA, tyto.SO.circular], + roles=[tyto.SO.plasmid_vector]) + + pib3_sc1 = sbol3.SubComponent(composite_part, roles=[tyto.SO.engineered_insert]) + gene_in_backbone.features += [pib3_sc1] + + pib3_sc2 = sbol3.SubComponent(backbone) + gene_in_backbone.features += [pib3_sc2] + + doc.add(gene_in_backbone) + + # Create component for assembly of BBa_K093005 + + assembly_comp = sbol3.Component('BBa_K093005_assembly', tyto.SBO.functional_entity) + + assembly_comp_sc1 = sbol3.SubComponent(assembled_rbs) + assembly_comp.features += [assembly_comp_sc1] + if failure_mode != 'BBa_E1010_UNASSEMBLED': + assembly_comp_sc2 = sbol3.SubComponent(assembled_cds) + assembly_comp.features += [assembly_comp_sc2] + if failure_mode != 'BBa_K093005_UNASSEMBLED': + assembly_comp_sc3 = sbol3.SubComponent(composite_part) + assembly_comp.features += [assembly_comp_sc3] + assembly_comp_sc4 = sbol3.SubComponent(rbs_in_backbone) + assembly_comp.features += [assembly_comp_sc4] + assembly_comp_sc5 = sbol3.SubComponent(cds_in_backbone) + assembly_comp.features += [assembly_comp_sc5] + assembly_comp_sc6 = sbol3.SubComponent(gene_in_backbone) + assembly_comp.features += [assembly_comp_sc6] + + pib_contains_rbs = sbol3.Constraint(sbol3.SBOL_CONTAINS, assembly_comp_sc4, assembly_comp_sc1) + assembly_comp.constraints += [pib_contains_rbs] + + if failure_mode != 'BBa_E1010_UNCONTAINED' and failure_mode != 'BBa_E1010_UNASSEMBLED': + pib_contains_cds = sbol3.Constraint(sbol3.SBOL_CONTAINS, assembly_comp_sc5, assembly_comp_sc2) + assembly_comp.constraints += [pib_contains_cds] + + if failure_mode != 'BBa_K093005_UNASSEMBLED': + pib_contains_gene = sbol3.Constraint(sbol3.SBOL_CONTAINS, assembly_comp_sc6, assembly_comp_sc3) + assembly_comp.constraints += [pib_contains_gene] + + doc.add(assembly_comp) + + # Create activity for assembly of BBa_K093005 + + assembly = sbol3.Activity('assemble_BBa_K093005', types=[sbol3.SBOL_DESIGN, SBOL_ASSEMBLY_PLAN]) + + assembly_usage = sbol3.Usage(assembly_comp.identity, roles=[sbol3.SBOL_DESIGN]) + assembly.usage += [assembly_usage] + + if failure_mode == 'EXTRA_ASSEMBLY_COMPONENT': + extra_assembly_comp = sbol3.Component('Extra_BBa_K093005_assembly', tyto.SBO.functional_entity) + + doc.add(extra_assembly_comp) + + extra_assembly_usage = sbol3.Usage(extra_assembly_comp.identity, roles=[sbol3.SBOL_DESIGN]) + assembly.usage += [extra_assembly_usage] + + doc.add(assembly) + + composite_part.generated_by += [assembly.identity] + + return composite_part + +if __name__ == '__main__': + unittest.main() \ No newline at end of file