diff --git a/Dockerfile b/Dockerfile index b1059ad..88f0710 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,7 @@ FROM continuumio/miniconda3 AS cheminf-python-ms ENV PYTHON_VERSION=3.10 ENV RDKIT_VERSION=2023.03.1 +ENV OPENBABEL_VERSION=v3.1 ARG RELEASE_VERSION ENV RELEASE_VERSION=${RELEASE_VERSION} @@ -12,8 +13,9 @@ RUN apt-get update && \ apt-get update -y && \ apt-get install -y openjdk-11-jre -RUN conda install -c conda-forge python>=$PYTHON_VERSION +RUN conda install -c conda-forge python>=PYTHON_VERSION RUN conda install -c conda-forge rdkit>=RDKIT_VERSION +RUN conda install -c conda-forge openbabel>=OPENBABEL_VERSION RUN python3 -m pip install -U pip diff --git a/app/modules/toolkits/cdkmodules.py b/app/modules/toolkits/cdkmodules.py index b15d541..bace67c 100644 --- a/app/modules/toolkits/cdkmodules.py +++ b/app/modules/toolkits/cdkmodules.py @@ -401,6 +401,46 @@ def getCXSMILES(smiles: str): return str(CXSMILES) +def getCanonSMILES(smiles: str): + """This function takes the user input SMILES and creates a + Canonical SMILES string with 2D atom coordinates + Args: + smiles (string): SMILES string given by the user. + Returns: + smiles (string): Canonical SMILES string. + + """ + moleculeSDG = getCDKSDG(smiles) + SmiFlavor = JClass(cdk_base + ".smiles.SmiFlavor") + SmilesGenerator = JClass(cdk_base + ".smiles.SmilesGenerator")(SmiFlavor.Absolute) + CanonicalSMILES = SmilesGenerator.create(moleculeSDG) + return str(CanonicalSMILES) + + +def getInChI(smiles: str, InChIKey=False): + """This function takes the user input SMILES and creates a + InChI string + Args: + smiles (string): SMILES string given by the user. + Returns: + smiles (string): InChI/InChIKey string. + + """ + moleculeSDG = getCDKSDG(smiles) + InChIGeneratorFactory = JClass(cdk_base + ".inchi.InChIGeneratorFactory") + InChI = ( + InChIGeneratorFactory.getInstance().getInChIGenerator(moleculeSDG).getInchi() + ) + if InChIKey: + InChIKey = ( + InChIGeneratorFactory.getInstance() + .getInChIGenerator(moleculeSDG) + .getInchiKey() + ) + return InChIKey + return InChI + + async def getCDKHOSECodes(smiles: str, noOfSpheres: int, ringsize: bool): """This function takes the user input SMILES and returns a mol block as a string with Structure Diagram Layout. diff --git a/app/modules/toolkits/openbabelmodules.py b/app/modules/toolkits/openbabelmodules.py new file mode 100644 index 0000000..9eac9de --- /dev/null +++ b/app/modules/toolkits/openbabelmodules.py @@ -0,0 +1,82 @@ +from openbabel import openbabel as ob +from openbabel import pybel + +def getOBCanonicalSMILES(smiles:str): + """This function takes an input as a SMILES string and + returns a Canonical SMILES. + Args (str): SMILES string. + Returns (str): Canonical SMILES string. + """ + if any(char.isspace() for char in smiles): + smiles = smiles.replace(" ", "+") + + # Create an Open Babel molecule object + mol = ob.OBMol() + + conv = ob.OBConversion() + conv.SetInAndOutFormats("smi", "can") + conv.ReadString(mol, smiles) + + canSMILES = conv.WriteString(mol) + canSMILES = canSMILES.strip() # Remove leading/trailing whitespace + return canSMILES + + +def getOBInChI(smiles:str,InChIKey:bool=False): + """This function takes an input as a SMILES string and + returns a InChI + Args (str): SMILES string. + Returns (str): InChI string. + """ + if any(char.isspace() for char in smiles): + smiles = smiles.replace(" ", "+") + + # Create an Open Babel molecule object + mol = ob.OBMol() + + # Create OBConversion + conv = ob.OBConversion() + conv.SetInAndOutFormats("smi", "inchi") + conv.ReadString(mol, smiles) + + inchi = conv.WriteString(mol) + inchi = inchi.strip() # Remove leading/trailing whitespace + if InChIKey: + conv.SetOptions("K", conv.OUTOPTIONS) + inchikey_ = conv.WriteString(mol).rstrip() + return inchikey_ + return inchi + +def getOBMol(smiles:str, threeD:bool=False): + """This function takes an input as a SMILES string and + returns a 2D/3D mol block. + Args (str): SMILES string. + Returns (str): Mol block (2D/3D). + """ + if any(char.isspace() for char in smiles): + smiles = smiles.replace(" ", "+") + + if threeD: + mol = pybel.readstring("smi", smiles) + mol.addh() + mol.make3D() + mol.removeh() + return mol.write("mol") + + # Create an Open Babel molecule object + mol = ob.OBMol() + + conv = ob.OBConversion() + conv.SetInAndOutFormats("smi", "mol") + conv.ReadString(mol, smiles) + + # Generate 2D coordinates + obBuilder = ob.OBBuilder() + obBuilder.Build(mol) + + mol_block = conv.WriteString(mol) + mol_block = mol_block.strip() # Remove leading/trailing whitespace + return mol_block + + + diff --git a/app/modules/toolkits/rdkitmodules.py b/app/modules/toolkits/rdkitmodules.py index c7de22a..52d0ba0 100644 --- a/app/modules/toolkits/rdkitmodules.py +++ b/app/modules/toolkits/rdkitmodules.py @@ -194,3 +194,21 @@ def has_stereochemistry(smiles: str): return True return False + + +def get2Dmol(smiles: str): + """This function takes an input as a SMILES string and + returns a 2D mol block. + Args (str): SMILES string. + Returns (str): 2D Mol block. + """ + if any(char.isspace() for char in smiles): + smiles = smiles.replace(" ", "+") + mol = Chem.MolFromSmiles(smiles) + + if mol: + AllChem.Compute2DCoords(mol) + molfile = Chem.MolToMolBlock(mol) + return molfile + else: + return "Error reading SMILES string, check again." diff --git a/app/routers/chem.py b/app/routers/chem.py index 2b4e8fc..c9ed74b 100644 --- a/app/routers/chem.py +++ b/app/routers/chem.py @@ -10,7 +10,6 @@ from app.modules.npscorer import getNPScore from app.modules.classyfire import classify, result from app.modules.toolkits.cdkmodules import ( - getCDKSDGMol, getTanimotoSimilarityCDK, getCDKHOSECodes, ) @@ -149,24 +148,6 @@ async def ClassyFire_result(id: str): return data -@router.get("/cdk2d") -async def CDK2D_Coordinates(smiles: str): - """ - Generate 2D Coordinates using the CDK Structure diagram generator and return the mol block. - - - **SMILES**: required (query) - """ - if smiles: - mol = Chem.MolFromSmiles(smiles) - if mol: - return Response( - content=getCDKSDGMol(smiles).replace("$$$$\n", ""), - media_type="text/plain", - ) - else: - return "Error reading SMILES string, check again." - - @router.get("/rdkit3d") async def RDKit3D_Mol(smiles: str): """ diff --git a/app/routers/converters.py b/app/routers/converters.py index d569a60..9a2354c 100644 --- a/app/routers/converters.py +++ b/app/routers/converters.py @@ -3,11 +3,20 @@ from fastapi import APIRouter from fastapi.responses import Response from rdkit import Chem -from rdkit.Chem import AllChem from typing import Optional from STOUT import translate_forward, translate_reverse -from app.modules.toolkits.cdkmodules import getCDKSDGMol, getCXSMILES -from app.modules.toolkits.rdkitmodules import get3Dconformers +from app.modules.toolkits.cdkmodules import ( + getCDKSDGMol, + getCXSMILES, + getCanonSMILES, + getInChI, +) +from app.modules.toolkits.rdkitmodules import get3Dconformers, get2Dmol +from app.modules.toolkits.openbabelmodules import ( + getOBMol, + getOBCanonicalSMILES, + getOBInChI, +) router = APIRouter( prefix="/convert", @@ -22,13 +31,12 @@ async def converters_index(): return {"module": "converters", "message": "Successful", "status": 200} -@router.get("/mol") -async def SMILES_Mol(smiles: str, generator: Optional[str] = "cdk"): +@router.get("/mol2D") +async def Create2D_Coordinates(smiles: str, generator: Optional[str] = "cdk"): """ - Convert SMILES to mol block: + Generate 2D Coordinates using the CDK Structure diagram generator/Rdkit/Openbabel and return the mol block. - - **SMILES**: required (query parameter) - - **generator**: optional (defaults: cdk) + - **SMILES**: required (query) """ if smiles: if generator: @@ -37,66 +45,91 @@ async def SMILES_Mol(smiles: str, generator: Optional[str] = "cdk"): content=getCDKSDGMol(smiles).replace("$$$$\n", ""), media_type="text/plain", ) + elif generator == "rdkit": + return Response( + content=get2Dmol(smiles), + media_type="text/plain", + ) else: - mol = Chem.MolFromSmiles(smiles) - if mol: - AllChem.Compute2DCoords(mol) - return Response( - content=Chem.MolToMolBlock(mol), media_type="text/plain" - ) - else: - return "Error reading SMILES string check again." - else: - return "Error reading SMILES string check again." + return Response( + content=getOBMol(smiles), + media_type="text/plain", + ) + else: + return "Error reading SMILES string, check again." -@router.get("/rdkit3d") -async def SMILES_Generate3DConformer(smiles: str): +@router.get("/mol3d") +async def Create3D_Coordinates(smiles: str, generator: Optional[str] = "rdkit"): """ - Generate a random 3D conformer from SMILES using RDKit: + Generate a random 3D conformer from SMILES using RDKit/OpenBabel. + CDK is not used for this purpose. - **SMILES**: required (query parameter) """ if smiles: - return Response( - content=get3Dconformers(smiles, depict=False), media_type="text/plain" - ) + if generator: + if generator == "rdkit": + return Response( + content=get3Dconformers(smiles, depict=False), + media_type="text/plain", + ) + elif generator == "openbabel": + return Response( + content=getOBMol(smiles, threeD=True), + media_type="text/plain", + ) + else: return "Error reading SMILES string check again." @router.get("/canonicalsmiles") -async def SMILES_Canonicalise(smiles: str): +async def SMILES_Canonicalise(smiles: str, generator: Optional[str] = "cdk"): """ - Cannonicalise SMILES: + Canonicalise SMILES. - **SMILES**: required (query parameter) """ if any(char.isspace() for char in smiles): smiles = smiles.replace(" ", "+") if smiles: - mol = Chem.MolFromSmiles(smiles) - if mol: - return Chem.MolToSmiles(mol) - else: - return "Error reading SMILES string check again." + if generator: + if generator == "cdk": + return str(getCanonSMILES(smiles)) + elif generator == "rdkit": + mol = Chem.MolFromSmiles(smiles) + if mol: + return Chem.MolToSmiles(mol, kekuleSmiles=True) + elif generator == "openbabel": + return getOBCanonicalSMILES(smiles) + + else: + return "Error reading SMILES string check again." else: return "Error reading SMILES string check again." @router.get("/inchi") -async def SMILES_to_InChI(smiles: str): +async def SMILES_to_InChI(smiles: str, generator: Optional[str] = "cdk"): """ - Convert SMILES to InChI: + Convert SMILES to InChI - **SMILES**: required (query parameter) """ if any(char.isspace() for char in smiles): smiles = smiles.replace(" ", "+") + if smiles: - mol = Chem.MolFromSmiles(smiles) - if mol: - return Chem.inchi.MolToInchi(mol) + if generator: + if generator == "cdk": + return str(getInChI(smiles)) + elif generator == "rdkit": + mol = Chem.MolFromSmiles(smiles) + if mol: + return Chem.inchi.MolToInchi(mol) + elif generator == "openbabel": + return getOBInChI(smiles) else: return "Error reading SMILES string check again." else: @@ -104,7 +137,7 @@ async def SMILES_to_InChI(smiles: str): @router.get("/inchikey") -async def SMILES_to_InChIKey(smiles: str): +async def SMILES_to_InChIKey(smiles: str, generator: Optional[str] = "cdk"): """ Convert SMILES to InChIKey: @@ -113,9 +146,15 @@ async def SMILES_to_InChIKey(smiles: str): if any(char.isspace() for char in smiles): smiles = smiles.replace(" ", "+") if smiles: - mol = Chem.MolFromSmiles(smiles) - if mol: - return Chem.inchi.MolToInchiKey(mol) + if generator: + if generator == "cdk": + return str(getInChI(smiles, InChIKey=True)) + elif generator == "rdkit": + mol = Chem.MolFromSmiles(smiles) + if mol: + return Chem.inchi.MolToInchiKey(mol) + elif generator == "openbabel": + return getOBInChI(smiles, InChIKey=True) else: return "Error reading SMILES string check again." else: @@ -139,7 +178,7 @@ async def SMILES_to_CXSMILES(smiles: str): @router.get("/formats") -async def SMILES_convert_to_Formats(smiles: str): +async def SMILES_convert_to_Formats(smiles: str, generator: Optional[str] = "cdk"): """ Convert SMILES to mol block: @@ -148,16 +187,35 @@ async def SMILES_convert_to_Formats(smiles: str): if any(char.isspace() for char in smiles): smiles = smiles.replace(" ", "+") if smiles: - mol = Chem.MolFromSmiles(smiles) - if mol: - response = {} - response["mol"] = Chem.MolToMolBlock(mol) - response["cannonicalsmiles"] = Chem.MolToSmiles(mol, kekuleSmiles=True) - response["inchi"] = Chem.inchi.MolToInchi(mol) - response["inchikey"] = Chem.inchi.MolToInchiKey(mol) - return response - else: - return "Error reading SMILES string check again." + if generator: + if generator == "cdk": + response = {} + response["mol"] = getCDKSDGMol(smiles).replace("$$$$\n", "") + response["canonicalsmiles"] = str(getCanonSMILES(smiles)) + response["inchi"] = str(getInChI(smiles)) + response["inchikey"] = str(getInChI(smiles, InChIKey=True)) + return response + + elif generator == "rdkit": + mol = Chem.MolFromSmiles(smiles) + if mol: + response = {} + response["mol"] = Chem.MolToMolBlock(mol) + response["canonicalsmiles"] = Chem.MolToSmiles( + mol, kekuleSmiles=True + ) + response["inchi"] = Chem.inchi.MolToInchi(mol) + response["inchikey"] = Chem.inchi.MolToInchiKey(mol) + return response + elif generator == "openbabel": + response = {} + response["mol"] = getOBMol(smiles) + response["canonicalsmiles"] = getOBCanonicalSMILES(smiles) + response["inchi"] = getOBInChI(smiles) + response["inchikey"] = getOBInChI(smiles, InChIKey=True) + return response + else: + return "Error reading SMILES string check again." else: return "Error reading SMILES string check again."