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