Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions sbol_utilities/build_planning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import sbol3
import tyto

from sbol_utilities.component import get_subcomponents, get_subcomponents_by_identity

# TODO: Change SBOL_ASSEMBLY_PLAN and sbol3.SBOL_DESIGN to tyto calls after resolution of
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 its structure follows the best practices for representing a part in backbone
"""
subcomps = get_subcomponents(pib)

has_insert = False
has_backbone = False

i = 0
while (not has_insert or not has_backbone) and i < len(subcomps):
part = subcomps[i].instance_of.lookup()

if tyto.SO.engineered_insert in subcomps[i].roles or tyto.SO.engineered_insert in part.roles:
has_insert = True
elif is_backbone(part):
has_backbone = True

i = i + 1

return has_insert and has_backbone
Copy link
Contributor

Choose a reason for hiding this comment

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

Should also check location/constraints.



def is_backbone(b: sbol3.Component) -> bool:
"""Check if Component is a backbone
Copy link
Contributor

Choose a reason for hiding this comment

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

Explain what is being checked for.


:param plan: Component being checked
:return: true if it has an expected role for a backbone
"""
for role in b.roles:
if role == tyto.SO.vector_replicon or tyto.SO.vector_replicon.is_ancestor_of(role):
Copy link
Contributor

Choose a reason for hiding this comment

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

This check doesn't conform to SEP 055

return True

return False


def validate_composite_part_assemblies(c: sbol3.Component) -> bool:
"""Check if a Component for a composite part has valid assemblies
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. Please explain what the function actually does in each of the functions here and below.


:param plan: Component being validated
:return: true if its assemblies follows the best practices for representing a composite part
"""
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 follows best practices for representing assembly of composite part
"""
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}

assembled_subcomps = get_subcomponents(composite_part)
assembled_ids = {str(sc.instance_of) for sc in assembled_subcomps}

has_composite = composite_part.identity in assembly_ids

if has_composite:
for assembly_subcomponent in assembly_subcomps:
if str(assembly_subcomponent.instance_of) == composite_part.identity:
composite_subid = assembly_subcomponent.identity

unassembled = assembled_ids.difference(assembly_ids)

assembled_subids = {sc.identity for sc in assembly_subcomps if str(sc.instance_of) in assembled_ids}

# TODO: Change sbol3.SBOL_CONTAINS to tyto call after resolution of
contained_map = {str(co.object) : str(co.subject) for co in ac.constraints if co.restriction == sbol3.SBOL_CONTAINS}

uncontained = assembled_subids.difference(contained_map.keys())
if composite_subid not in contained_map.keys():
uncontained.add(composite_subid)

pib_subids = [contained_map[key] for key in contained_map.keys()
if key in assembled_subids or key == composite_subid]
else:
unassembled = assembled_ids.difference(assembly_ids)

assembled_subids = {sc.identity for sc in assembly_subcomps if str(sc.instance_of) in assembled_ids}

# TODO: Change sbol3.SBOL_CONTAINS to tyto call after resolution of
contained_map = {str(co.object) : str(co.subject) for co in ac.constraints if co.restriction == sbol3.SBOL_CONTAINS}

uncontained = assembled_subids.difference(contained_map.keys())

pib_subids = [contained_map[key] for key in contained_map.keys() if key in assembled_subids]

parts_in_backbones = [sc.instance_of.lookup() for sc in get_subcomponents_by_identity(ac, pib_subids)]

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 follows best practices for representing assembly of composite part
"""
# TODO: Change sbol3.SBOL_DESIGN to tyto call after resolution of

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
8 changes: 8 additions & 0 deletions sbol_utilities/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
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

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]:
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
152 changes: 152 additions & 0 deletions test/test_build_planning.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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_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')

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


assert not validate_composite_part_assemblies(assemble_BBa_K093005(test_doc, 'BBa_E1010_UNASSEMBLED'))
assert not validate_composite_part_assemblies(assemble_BBa_K093005(test_doc, 'BBa_K093005_UNASSEMBLED'))
assert not validate_composite_part_assemblies(assemble_BBa_K093005(test_doc, 'BBa_E1010_UNCONTAINED'))
assert not validate_composite_part_assemblies(assemble_BBa_K093005(test_doc, 'BBa_E1010_NOT_INSERT'))
assert not validate_composite_part_assemblies(assemble_BBa_K093005(test_doc, 'pSB1C3_NOT_BACKBONE'))
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()