Skip to content
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

Functions for Validating Composite Part Assemblies Plus Tests #117

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open
135 changes: 135 additions & 0 deletions sbol_utilities/build_planning.py
Original file line number Diff line number Diff line change
@@ -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
jakebeal marked this conversation as resolved.
Show resolved Hide resolved

: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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be done more efficiently by applying any to a generator.



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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need comments all through here to explain what you're doing and why

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]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need comments all through here to explain what you're doing and why.


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
18 changes: 18 additions & 0 deletions sbol_utilities/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@
from sbol_utilities.workarounds import get_parent


def get_subcomponents(c: sbol3.Component) -> List[sbol3.SubComponent]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing docstrings

"""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.
Expand Down
169 changes: 169 additions & 0 deletions test/test_build_planning.py
Original file line number Diff line number Diff line change
@@ -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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing docstrings


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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explain why these should or shouldn't work in comments


# 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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above; need comments to explain your testing


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()