diff --git a/sbol_utilities/helper_functions.py b/sbol_utilities/helper_functions.py index 168debfc..15663962 100644 --- a/sbol_utilities/helper_functions.py +++ b/sbol_utilities/helper_functions.py @@ -363,4 +363,31 @@ def is_circular(obj: Union[sbol3.Component, sbol3.LocalSubComponent, sbol3.Exter :param obj: design to be checked :return: true if circular """ - return any(n==sbol3.SO_CIRCULAR for n in obj.types) \ No newline at end of file + return any(n==sbol3.SO_CIRCULAR for n in obj.types) + +def is_composite(obj): + """Check if an SBOL Component is a composite. + + A composite component is defined as: + 1. Having a DNA type. + 2. Being generated by an assembly plan activity. + + :param obj: SBOL Component to be checked. + :return: True if the component is composite, otherwise False. + """ + + def has_dna_type(o): + """Check if the component has a DNA type.""" + return any(tyto.SO.DNA == tyto.SO.get_uri_by_term(t) for t in o.types) + + def has_assembly_plan(o): + """Check if the component was generated by an assembly plan.""" + for activity_ref in o.generated_by: + activity = activity_ref.lookup() + if activity and 'http://sbols.org/v3#assemblyPlan' in activity.types and sbol3.SBOL_DESIGN in activity.types: + return True + return False + + if isinstance(obj, sbol3.Component): + return has_dna_type(obj) and has_assembly_plan(obj) + return False diff --git a/test/test_helpers.py b/test/test_helpers.py index 3bc5605f..f05ea068 100644 --- a/test/test_helpers.py +++ b/test/test_helpers.py @@ -1,6 +1,8 @@ import unittest import os from pathlib import Path +import sbol3 +import tyto from sbol_utilities import component @@ -31,6 +33,36 @@ def test_url_sanitization(self): self.assertEqual(strip_filetype_suffix('http://foo/bar/baz.gb'), 'http://foo/bar/baz') self.assertEqual(strip_filetype_suffix('http://foo/bar/baz.qux'), 'http://foo/bar/baz.qux') + def test_is_composite(self): + """Test the is_composite function.""" + # Set up a test SBOL document and namespace + doc = sbol3.Document() + sbol3.set_namespace('http://sbolstandard.org/test') + # Case 1: Valid composite component (Has DNA type + Assembly Plan) + comp1 = sbol3.Component('comp1', types=[tyto.SO.DNA]) + assembly_activity = sbol3.Activity('activity1') + assembly_activity.types.append("http://sbols.org/v3#assemblyPlan") + assembly_activity.types.append(sbol3.SBOL_DESIGN) + # Add activity to the document + doc.add(assembly_activity) + comp1.generated_by.append(sbol3.ReferencedObject(assembly_activity.identity)) + doc.add(comp1) + self.assertTrue(is_composite(comp1), "Expected comp1 to be composite") + # Case 2: Not composite (No DNA type, but has Assembly Plan) + comp2 = sbol3.Component('comp2', types=[tyto.SO.RNA]) # RNA type instead of DNA + comp2.generated_by.append(sbol3.ReferencedObject(assembly_activity.identity)) + doc.add(comp2) + self.assertFalse(is_composite(comp2), "Expected comp2 to NOT be composite") + # Case 3: Not composite (Has DNA type, but no Assembly Plan) + comp3 = sbol3.Component('comp3', types=[tyto.SO.DNA]) + doc.add(comp3) + self.assertFalse(is_composite(comp3), "Expected comp3 to NOT be composite") + # Case 4: Not composite (No DNA type, No Assembly Plan) + comp4 = sbol3.Component('comp4', types=[tyto.SO.RNA]) + doc.add(comp4) + self.assertFalse(is_composite(comp4), "Expected comp4 to NOT be composite") + + def test_filtering_top_level_objects(self): """Check filtering Top Level Objects by a condition""" test_dir = os.path.dirname(os.path.realpath(__file__))