Skip to content

Commit

Permalink
Merge pull request #215 from SynBioDex/native-sbol23-conversion
Browse files Browse the repository at this point in the history
First steps of SBOL3 to SBOL2 conversion in Python
  • Loading branch information
jakebeal authored Oct 7, 2023
2 parents ae3d852 + 3c4765a commit 6d59395
Show file tree
Hide file tree
Showing 4 changed files with 368 additions and 2 deletions.
14 changes: 12 additions & 2 deletions sbol_utilities/conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from sbol_utilities.helper_functions import strip_sbol2_version, GENETIC_DESIGN_FILE_TYPES, \
find_top_level
from sbol_utilities.sbol3_genbank_conversion import GenBankSBOL3Converter
import sbol_utilities.sbol3_sbol2_conversion
from sbol_utilities.workarounds import id_sort

# sbol javascript executable based on https://github.com/sboltools/sbolgraph
Expand Down Expand Up @@ -69,13 +70,18 @@ def convert_identities2to3(sbol3_data: str) -> str:
return g.serialize(format="xml")


def convert2to3(sbol2_doc: Union[str, sbol2.Document], namespaces=None) -> sbol3.Document:
def convert2to3(sbol2_doc: Union[str, sbol2.Document], namespaces=None, use_native_converter: bool = False) \
-> sbol3.Document:
"""Convert an SBOL2 document to an equivalent SBOL3 document
:param sbol2_doc: Document to convert
:param namespaces: list of URI prefixes to treat as namespaces
:param use_native_converter: if true, use experimental Python converter instead of JavaScript call-out
:return: equivalent SBOL3 document
"""
if use_native_converter:
return sbol_utilities.sbol3_sbol2_conversion.convert2to3(sbol2_doc)

# if we've started with a Document in memory, write it to a temp file
if namespaces is None:
namespaces = []
Expand Down Expand Up @@ -167,12 +173,16 @@ def change_orientation(o):
return doc


def convert3to2(doc3: sbol3.Document) -> sbol2.Document:
def convert3to2(doc3: sbol3.Document, use_native_converter: bool = False) -> sbol2.Document:
"""Convert an SBOL3 document to an equivalent SBOL2 document
:param doc3: Document to convert
:param use_native_converter: if true, use experimental Python converter instead of JavaScript call-out
:return: equivalent SBOL2 document
"""
if use_native_converter:
return sbol_utilities.sbol3_sbol2_conversion.convert3to2(doc3)

# TODO: remove workarounds after conversion errors fixed in https://github.com/sboltools/sbolgraph/issues/16
# remap sequence encodings:
encoding_remapping = {
Expand Down
264 changes: 264 additions & 0 deletions sbol_utilities/sbol3_sbol2_conversion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
import sbol3
import sbol2

# Namespaces
from rdflib import URIRef

BACKPORT_NAMESPACE = 'http://sboltools.org/backport#'
BACKPORT2_VERSION = f'{BACKPORT_NAMESPACE}sbol2version'
BACKPORT3_NAMESPACE = f'{BACKPORT_NAMESPACE}sbol3namespace'

NON_EXTENSION_PROPERTY_PREFIXES = {sbol3.SBOL3_NS, sbol3.SBOL2_NS, # SBOL 2 & 3 namespaces
sbol3.RDF_NS, sbol3.PROV_NS, sbol3.OM_NS, # Standard ontologies
BACKPORT_NAMESPACE} # Information added by this converter


class SBOL3To2ConversionVisitor:
"""This class is used to map every object in an SBOL3 document into an empty SBOL2 document"""

doc2: sbol2.Document

def __init__(self, doc3: sbol3.Document):
# Create the target document
self.doc2 = sbol2.Document()
# # Immediately run the conversion
self._convert(doc3)

def _convert(self, doc3: sbol3.Document):
# Bind standard namespaces that aren't bound by default in pySBOL2
self.doc2.addNamespace(BACKPORT_NAMESPACE, 'backport')
self.doc2.addNamespace(sbol3.PROV_NS, 'prov')
self.doc2.addNamespace(sbol3.OM_NS, 'om')
self.doc2.addNamespace('http://purl.org/dc/terms/', 'dcterms')

# Override parameters that will otherwise interfere in conversion, saving old values
saved_compliance = sbol2.Config.getOption(sbol2.ConfigOptions.SBOL_COMPLIANT_URIS.value)
sbol2.Config.setOption(sbol2.ConfigOptions.SBOL_COMPLIANT_URIS.value, False)
saved_homespace = sbol2.getHomespace()
sbol2.setHomespace('')

# Try conversion, resetting saved parameter values afterward
try:
doc3.accept(self)
# TODO: make sure that complex extension objects (e.g., from SBOLFactory) are properly converted
# TODO: make sure that unhandled SBOL child objects / properties will throw errors
# TODO: check if we need to add post-creation fix-up of links, to ensure they point to objects
finally:
sbol2.Config.setOption(sbol2.ConfigOptions.SBOL_COMPLIANT_URIS.value, saved_compliance)
sbol2.setHomespace(saved_homespace)

@staticmethod
def _convert_extension_properties(obj3: sbol3.Identified, obj2: sbol2.Identified):
"""Copy over extension properties"""
extension_properties = (p for p in obj3.properties
if not any(p.startswith(prefix) for prefix in NON_EXTENSION_PROPERTY_PREFIXES))
for p in extension_properties:
obj2.properties[p] = obj3._properties[p] # Can't use setPropertyValue because it may not be a string

def _convert_identified(self, obj3: sbol3.Identified, obj2: sbol2.Identified):
"""Map over the other properties of an identified object"""
self._convert_extension_properties(obj3, obj2)
# Map over equivalent properties
obj2.displayId = obj3.display_id
obj2.name = obj3.name
obj2.description = obj3.description
obj2.wasDerivedFrom = obj3.derived_from
obj2.wasGeneratedBy = obj3.generated_by
# Turn measures into extension properties
if obj3.measures:
raise NotImplementedError('Conversion of measures from SBOL3 to SBOL2 not yet implemented')
pass

def _convert_toplevel(self, obj3: sbol3.TopLevel, obj2: sbol2.TopLevel):
"""Map over the other properties of a TopLevel object"""
self._convert_identified(obj3, obj2)
obj2.attachments = [a.identity for a in obj3.attachments]
obj2.properties[BACKPORT3_NAMESPACE] = [URIRef(obj3.namespace)]

@staticmethod
def _sbol2_version(obj: sbol3.Identified):
obj.sbol2_version = sbol3.TextProperty(obj, BACKPORT2_VERSION, 0, 1)
return obj.sbol2_version or '1'

def visit_activity(self, act3: sbol3.Activity):
# Make the Activity object and add it to the document
act2 = sbol2.Activity(act3.identity, version=self._sbol2_version(act3))
if act3.types:
if len(act3.types) > 1:
raise NotImplementedError('Conversion of multi-type Activities to SBOL2 not yet implemented:'
'pySBOL2 currently supports a maximum of one type per activity'
'Bug: https://github.com/SynBioDex/pySBOL2/issues/428')
act2.types = act3.types[0] # Take first type from list of length 1
act2.startedAtTime = act3.start_time
act2.endedAtTime = act3.end_time
if act3.usage or act3.association:
raise NotImplementedError('Conversion of Activity usage and association properties to SBOL2 '
'not yet implemented, due to visitors failing to return values'
'Bug: https://github.com/SynBioDex/pySBOL3/issues/437')
act2.usages = [usage.accept(self) for usage in act3.usage]
act2.associations = [assoc.accept(self) for assoc in act3.association]
# TODO: pySBOL3 is currently missing wasInformedBy (https://github.com/SynBioDex/pySBOL3/issues/436
# act2.wasInformedBy = act3.informed_by
self.doc2.activities.add(act2)
# Map over all other TopLevel properties and extensions not covered by the constructor
self._convert_toplevel(act3, act2)

def visit_agent(self, a: sbol3.Agent):
raise NotImplementedError('Conversion of Agent from SBOL3 to SBOL2 not yet implemented')

def visit_association(self, a: sbol3.Association):
raise NotImplementedError('Conversion of Association from SBOL3 to SBOL2 not yet implemented')

def visit_attachment(self, a: sbol3.Attachment):
raise NotImplementedError('Conversion of Attachment from SBOL3 to SBOL2 not yet implemented')

def visit_binary_prefix(self, a: sbol3.BinaryPrefix):
raise NotImplementedError('Conversion of BinaryPrefix from SBOL3 to SBOL2 not yet implemented')

def visit_collection(self, a: sbol3.Collection):
raise NotImplementedError('Conversion of Collection from SBOL3 to SBOL2 not yet implemented')

def visit_combinatorial_derivation(self, a: sbol3.CombinatorialDerivation):
raise NotImplementedError('Conversion of CombinatorialDerivation from SBOL3 to SBOL2 not yet implemented')

def visit_component(self, cp3: sbol3.Component):
# Remap type if it's one of the ones that needs remapping; otherwise pass through unchanged
type_map = {sbol3.SBO_DNA: sbol2.BIOPAX_DNA, # TODO: distinguish BioPAX Dna from DnaRegion
sbol3.SBO_RNA: sbol2.BIOPAX_RNA, # TODO: distinguish BioPAX Rna from RnaRegion
sbol3.SBO_PROTEIN: sbol2.BIOPAX_PROTEIN,
sbol3.SBO_SIMPLE_CHEMICAL: sbol2.BIOPAX_SMALL_MOLECULE,
sbol3.SBO_NON_COVALENT_COMPLEX: sbol2.BIOPAX_COMPLEX}
types2 = [type_map.get(t, t) for t in cp3.types]
# Make the Component object and add it to the document
cp2 = sbol2.ComponentDefinition(cp3.identity, types2, version=self._sbol2_version(cp3))
self.doc2.addComponentDefinition(cp2)
# Convert the Component properties not covered by the constructor
cp2.roles = cp3.roles
cp2.sequences = cp3.sequences
if cp3.features:
raise NotImplementedError('Conversion of Component features from SBOL3 to SBOL2 not yet implemented')
if cp3.interactions:
raise NotImplementedError('Conversion of Component interactions from SBOL3 to SBOL2 not yet implemented')
if cp3.constraints:
raise NotImplementedError('Conversion of Component constraints from SBOL3 to SBOL2 not yet implemented')
if cp3.interface:
raise NotImplementedError('Conversion of Component interface from SBOL3 to SBOL2 not yet implemented')
if cp3.models:
raise NotImplementedError('Conversion of Component models from SBOL3 to SBOL2 not yet implemented')
# Map over all other TopLevel properties and extensions not covered by the constructor
self._convert_toplevel(cp3, cp2)

def visit_component_reference(self, a: sbol3.ComponentReference):
raise NotImplementedError('Conversion of ComponentReference from SBOL3 to SBOL2 not yet implemented')

def visit_constraint(self, a: sbol3.Constraint):
raise NotImplementedError('Conversion of Constraint from SBOL3 to SBOL2 not yet implemented')

def visit_cut(self, a: sbol3.Cut):
raise NotImplementedError('Conversion of Cut from SBOL3 to SBOL2 not yet implemented')

def visit_document(self, doc3: sbol3.Document):
for obj in doc3.objects:
obj.accept(self)

def visit_entire_sequence(self, a: sbol3.EntireSequence):
raise NotImplementedError('Conversion of EntireSequence from SBOL3 to SBOL2 not yet implemented')

def visit_experiment(self, a: sbol3.Experiment):
raise NotImplementedError('Conversion of Experiment from SBOL3 to SBOL2 not yet implemented')

def visit_experimental_data(self, a: sbol3.ExperimentalData):
raise NotImplementedError('Conversion of ExperimentalData from SBOL3 to SBOL2 not yet implemented')

def visit_externally_defined(self, a: sbol3.ExternallyDefined):
raise NotImplementedError('Conversion of ExternallyDefined from SBOL3 to SBOL2 not yet implemented')

def visit_implementation(self, a: sbol3.Implementation):
raise NotImplementedError('Conversion of Implementation from SBOL3 to SBOL2 not yet implemented')

def visit_interaction(self, a: sbol3.Interaction):
raise NotImplementedError('Conversion of Interaction from SBOL3 to SBOL2 not yet implemented')

def visit_interface(self, a: sbol3.Interface):
raise NotImplementedError('Conversion of Interface from SBOL3 to SBOL2 not yet implemented')

def visit_local_sub_component(self, a: sbol3.LocalSubComponent):
raise NotImplementedError('Conversion of LocalSubComponent from SBOL3 to SBOL2 not yet implemented')

def visit_measure(self, a: sbol3.Measure):
raise NotImplementedError('Conversion of Measure from SBOL3 to SBOL2 not yet implemented')

def visit_model(self, a: sbol3.Model):
raise NotImplementedError('Conversion of Model from SBOL3 to SBOL2 not yet implemented')

def visit_participation(self, a: sbol3.Participation):
raise NotImplementedError('Conversion of Participation from SBOL3 to SBOL2 not yet implemented')

def visit_plan(self, a: sbol3.Plan):
raise NotImplementedError('Conversion of Plan from SBOL3 to SBOL2 not yet implemented')

def visit_prefixed_unit(self, a: sbol3.PrefixedUnit):
raise NotImplementedError('Conversion of PrefixedUnit from SBOL3 to SBOL2 not yet implemented')

def visit_range(self, a: sbol3.Range):
raise NotImplementedError('Conversion of Range from SBOL3 to SBOL2 not yet implemented')

def visit_si_prefix(self, a: sbol3.SIPrefix):
raise NotImplementedError('Conversion of SIPrefix from SBOL3 to SBOL2 not yet implemented')

def visit_sequence(self, seq3: sbol3.Sequence):
# Remap encoding if it's one of the ones that needs remapping; otherwise pass through unchanged
encoding_map = {sbol3.IUPAC_DNA_ENCODING: sbol2.SBOL_ENCODING_IUPAC,
sbol3.IUPAC_PROTEIN_ENCODING: sbol2.SBOL_ENCODING_IUPAC_PROTEIN,
sbol3.SMILES_ENCODING: sbol2.SBOL_ENCODING_SMILES}
encoding2 = encoding_map.get(seq3.encoding, seq3.encoding)
# Make the Sequence object and add it to the document
seq2 = sbol2.Sequence(seq3.identity, seq3.elements, encoding=encoding2, version=self._sbol2_version(seq3))
self.doc2.addSequence(seq2)
# 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):
raise NotImplementedError('Conversion of SequenceFeature from SBOL3 to SBOL2 not yet implemented')

def visit_singular_unit(self, a: sbol3.SingularUnit):
raise NotImplementedError('Conversion of SingularUnit from SBOL3 to SBOL2 not yet implemented')

def visit_sub_component(self, a: sbol3.SubComponent):
raise NotImplementedError('Conversion of SubComponent from SBOL3 to SBOL2 not yet implemented')

def visit_unit_division(self, a: sbol3.UnitDivision):
raise NotImplementedError('Conversion of UnitDivision from SBOL3 to SBOL2 not yet implemented')

def visit_unit_exponentiation(self, a: sbol3.UnitExponentiation):
raise NotImplementedError('Conversion of UnitExponentiation from SBOL3 to SBOL2 not yet implemented')

def visit_unit_multiplication(self, a: sbol3.UnitMultiplication):
raise NotImplementedError('Conversion of UnitMultiplication from SBOL3 to SBOL2 not yet implemented')

def visit_usage(self, a: sbol3.Usage):
raise NotImplementedError('Conversion of Usage from SBOL3 to SBOL2 not yet implemented')

def visit_variable_feature(self, a: sbol3.VariableFeature):
raise NotImplementedError('Conversion of VariableFeature from SBOL3 to SBOL2 not yet implemented')


def convert3to2(doc3: sbol3.Document) -> sbol2.Document:
"""Convert an SBOL3 document to an SBOL2 document
:param doc3: SBOL3 document to convert
:returns: SBOL2 document
"""
converter = SBOL3To2ConversionVisitor(doc3)
return converter.doc2


def convert2to3(doc2: sbol2.Document) -> sbol3.Document:
"""Convert an SBOL2 document to an SBOL3 document
:param doc2: SBOL2 document to convert
:returns: SBOL3 document
"""
doc3 = sbol3.Document()
raise NotImplementedError('Conversion from SBOL2 to SBOL3 not yet implemented')
return doc3
Loading

0 comments on commit 6d59395

Please sign in to comment.