diff --git a/sbol_utilities/sbol3_sbol2_conversion.py b/sbol_utilities/sbol3_sbol2_conversion.py
index 19485ecb..e56aeb4b 100644
--- a/sbol_utilities/sbol3_sbol2_conversion.py
+++ b/sbol_utilities/sbol3_sbol2_conversion.py
@@ -389,9 +389,22 @@ def visit_sequence(self, seq3: sbol3.Sequence):
# Map over all other TopLevel properties and extensions not covered by the constructor
self._convert_toplevel(seq3, seq2)
- def visit_sequence_feature(self, a: sbol3.SequenceFeature):
- # Priority: 1
- raise NotImplementedError('Conversion of SequenceFeature from SBOL3 to SBOL2 not yet implemented')
+ def visit_sequence_feature(self, seqfeat3: sbol3.SequenceFeature):
+ # Priority: 1
+ # SBOL 2.x SequenceAnnotation objects map to SBOL 3.x SequenceFeature objects if they do not have a component.
+ # If they do have a component, their locations are added to the corresponding SBOL3 SubComponent.
+
+ # convert locations
+ locations2 = [loc3.accept(self) for loc3 in seqfeat3.locations]
+
+ # Create SBOL2 SequenceAnnotation
+ seqanno2 = sbol2.SequenceAnnotation(uri=self._sbol3_identity(seqfeat3),
+ locations=locations2,
+ roles=seqfeat3.roles,
+ version=self._sbol2_version(seqfeat3)
+ )
+ self._convert_identified(seqfeat3, seqanno2)
+ return seqanno2, locations2
def visit_singular_unit(self, a: sbol3.SingularUnit):
# Priority: 4
@@ -752,7 +765,7 @@ def visit_range(self, r2: sbol2.Range):
cdef = r2.parent.parent
ns = self._sbol3_namespace(cdef)
seq_stub = sbol3.Sequence(f'{ns}/{cdef.displayId}Seq/', namespace=ns)
- cdef.sequence = seq_stup
+ cdef.sequence = seq_stub
cdef.doc.add(seq_stub)
r3 = sbol3.Range(seq_ref, r2.start, r2.end)
self._convert_identified(r2, r3)
@@ -772,21 +785,35 @@ def visit_sequence(self, seq2: sbol2.Sequence):
# Map over all other TopLevel properties and extensions not covered by the constructor
self._convert_toplevel(seq2, seq3)
- def visit_sequence_annotation(self, sa2: sbol2.SequenceAnnotation):
- # component URIRef 0..1
- # orientation URI 0..1
- locations = []
- for l2 in sa2.locations:
- if type(l2) == sbol2.Range:
- l3 = self.visit_range(l2)
- else:
- raise NotImplementedError('Conversion of {type(l2)} from SBOL2 to SBOL3 not yet implemented')
- locations.append(l3)
-
- f3 = sbol3.SequenceFeature(locations)
- f3.roles = sa2.roles
- self._convert_identified(sa2, f3)
- return f3, locations
+ def visit_sequence_annotation(self, seqanno2: sbol2.SequenceAnnotation):
+ # Priority: 1
+ # SBOL 2.x SequenceAnnotation objects map to SBOL 3.x SequenceFeature objects if they do not have a component.
+ # If they do have a component, their locations are added to the corresponding SBOL3 SubComponent.
+ def _handle_locations(loc2):
+ if type(loc2) is sbol2.location.Range:
+ return self.visit_range(loc2)
+ elif type(loc2) is sbol2.location.Cut:
+ raise NotImplementedError('Conversion of Cut from SBOL2 to SBOL3 not yet implemented')
+ elif type(loc2) is sbol2.location.GenericLocation:
+ raise NotImplementedError('Conversion of GenericLocation from SBOL2 to SBOL3 not yet implemented')
+ else: raise ValueError(f'Unknown location type {type(loc2)}, SequenceAnnotation cannot convert to SBOL3')
+ # convert locations
+ locations3 = [_handle_locations(loc2) for loc2 in seqanno2.locations]
+
+ # Create SBOL3 SequenceFeature
+ if seqanno2.component:
+ feature3 = sbol3.SubComponent(instance_of=seqanno2.component, #TODO: verify if this is correct
+ locations=locations3,
+ roles=seqanno2.roles,
+ )
+ else:
+ feature3 = sbol3.SequenceFeature(identity=self._sbol3_identity(seqanno2),
+ locations=locations3,
+ roles=seqanno2.roles,
+ )
+ self._convert_identified(seqanno2, feature3)
+ return feature3, locations3
+
def visit_sequence_constraint(self, seq2: sbol2.sequenceconstraint.SequenceConstraint):
# Priority: 2
diff --git a/test/test_files/sbol_3to2_sequence_annotation.nt b/test/test_files/sbol_3to2_sequence_annotation.nt
new file mode 100644
index 00000000..2cca23e4
--- /dev/null
+++ b/test/test_files/sbol_3to2_sequence_annotation.nt
@@ -0,0 +1,33 @@
+
+
+
+ gattaca
+ sequence1
+
+
+
+
+ component1
+
+
+
+
+ Range1
+
+ 3
+ 1
+
+
+ SequenceFeature1
+ promoter_feature
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/test_files/sbol_3to2_sequence_feature.xml b/test/test_files/sbol_3to2_sequence_feature.xml
new file mode 100644
index 00000000..fd14da67
--- /dev/null
+++ b/test/test_files/sbol_3to2_sequence_feature.xml
@@ -0,0 +1,35 @@
+
+
+
+ component1
+
+
+
+
+
+
+
+ sequence1
+
+
+ gattaca
+
+
+
+ Range1
+
+
+ 1
+ 3
+
+
+ SequenceFeature1
+ promoter_feature
+
+
+
+
+
diff --git a/test/test_sbol2_sbol3_direct.py b/test/test_sbol2_sbol3_direct.py
index 74ba6acf..3cedadf8 100644
--- a/test/test_sbol2_sbol3_direct.py
+++ b/test/test_sbol2_sbol3_direct.py
@@ -309,6 +309,9 @@ def test_functionalcomponent_conversion(self):
def test_interaction_conversion(self):
self.handle_2to3_conversion('sbol_3to2_interaction.xml', 'sbol_3to2_interaction.nt')
+ def test_seq_annotation_conversion(self):
+ self.handle_2to3_conversion('sbol_3to2_sequence_annotation.nt', 'sbol_3to2_sequence_feature.nt')
+
class TestDirectSBOL3SBOL2Conversion(unittest.TestCase):
@@ -373,6 +376,8 @@ def test_identity_conversion(self):
def test_interaction_conversion(self):
self.handle_3to2_conversion('sbol_3to2_interaction.nt', 'sbol_3to2_interaction.xml')
-
+ def test_seq_feature_conversion(self):
+ self.handle_3to2_conversion('sbol_3to2_sequence_feature.xml', 'sbol_3to2_sequence_annotation.nt')
+
if __name__ == '__main__':
unittest.main()