Package: uamc-spectrophore
SpectrophoresThis module contains python code to calculatespectrophoresfrom molecules. It is using theRDKitandnumbatoolkits.The technology and its applications have been described inJournal of Cheminformatics(2018)10, 9. The paper is also included in this distribution.Thespectrophorecode can be used in two ways:As a standalone program to convert the molecules in a sd-file into their correspondingspectrophores;As apythonmodule to import in your ownpythoncode.In the following sections, both usages will be documented.Installation1. Installation of RDKit and NumbaWe recommend to install bothRDKitandNumbausingAnaconda. Ifcondais not yet available on your system, you should install this first following the instructions on theAnaconda website.Numbais an open source JIT compiler that translates a subset of Python and NumPy code into fast machine code.RDKitis open source cheminformatics software that provides the code to work with molecules.The easiest way to have everything installed is usingconda. First create a suitable environment in which you will install thespectrophoretechnology:> conda create --name spectrophore python=3This will install a newcondaenvironment with Python 3.9 installed in it. Now activate this environment:> conda activate spectrophoreNumbaandrdkitcan now be installed as follows (make sure you first activated thespectrophoreenvironment):> conda install numba> conda install cudatoolkit> conda install -c conda-forge rdkitYou can test therdkitinstallation by opening apythonsession from your command-line (assuming you are still in the activatedspectrophoreenvironment) and typing the following:>>>fromrdkitimportChem>>>mol=Chem.MolFromSmiles("C1CCCC1")>>>print(mol.GetNumAtoms())5Similarly, you can test thenumbainstallation with this smallpythonsnippet:>>>importnumba>>>numba.__version__'0.51.2'Withnumba -scommand you can also check whether you have a CUDA device installed (check for the section__CUDA Information__).2. Installing the spectrophore codeWith thespectrophoreenvironment still active, you can now easily install thespectrophoremodule using:> pip install uamc-spectrophoreCheck the installation by opening apythonsession and entering:>>>fromspectrophoreimportspectrophore>>>spectrophore.__version__'1.2.0'1. Usage as a standalone programAfter installation, one should be able to use thespectrophore.pycode as a standalone program to calculatedspectrophoresfrom a sd-file with molecules. You can find out wherespectrophore.pyis located by starting apythonshell and typing:>>>fromspectrophoreimportspectrophore>>>spectrophore.__file__'/Users/hans/anaconda3/envs/spectrophore/lib/python3.8/site-packages/spectrophore/spectrophore.py'Either you can use this full path to call thespectrophore.pycode, or you can add it to your $PATH environment variable.To usespectrophore.py, type the following on the command-line:> spectrophore.py -hThis will provide you with all details on how to calculatespectrophoresfrom a sd-file:usage: spectrophore.py [-h] [-n {none,mean,all,std}] [-s {none,unique,mirror,all}] [-a {1,2,3,4,5,6,9,10,12,15,18,20,30,36,45,60,90,180}][-r RESOLUTION] [-p MAX_WORKERS] -i INFILE -o OUTFILECalculate spectrophoresoptional arguments:-h, --help            show this help message and exit-n {none,mean,all,std}, --norm {none,mean,all,std}normalization setting (default: all)-s {none,unique,mirror,all}, --stereo {none,unique,mirror,all}stereo setting (default: none)-a {1,2,3,4,5,6,9,10,12,15,18,20,30,36,45,60,90,180}, --accuracy {1,2,3,4,5,6,9,10,12,15,18,20,30,36,45,60,90,180}accuracy setting (default: 20)-r RESOLUTION, --resolution RESOLUTIONresolution setting (>0) (default: 3)-p MAX_WORKERS, --np MAX_WORKERSnumber of processors to use; -1 is all processors (default: -1)required arguments:-i INFILE, --in INFILEinput sdf file (default: None)-o OUTFILE, --out OUTFILEoutput spectrophore file (default: None)2. Usage as apythonmodule2.1. IntroductionOnce you have installed all required tools and theuamc-spectrophorepackage, you are ready to use the tool. In its most simple form,spectrophorescan be calculated as follows:>>>fromspectrophoreimportspectrophore>>>fromrdkitimportChem>>>fromrdkit.ChemimportAllChem>>>mol=Chem.MolFromSmiles("c1ncncc1")>>>mol=Chem.AddHs(mol)>>>AllChem.EmbedMolecule(mol,randomSeed=1)0>>>calculator=spectrophore.SpectrophoreCalculator(normalization='none')Probesinitialised:48numberofprobesintotal12probesareusedduetotheimposedstereoflag>>>calculator.calculate(mol)array([1.409246,2.021652,1.6011626,3.034698,2.4150815,5.0872273,2.285813,1.7250485,3.436644,4.0012817,5.092206,2.9844987,0.6417792,0.8024898,4.8707156,4.870761,2.8789856,4.104702,1.9413302,3.5960448,4.9019723,4.151822,4.5394773,5.766127,44.79124,71.551796,106.82244,106.82059,49.73703,61.662792,23.50798,81.88448,77.47026,67.52185,57.44229,112.96884,0.6794604,1.1607243,2.470075,2.470103,1.0203087,1.1483352,0.51142335,1.7433033,1.8094715,1.3015395,1.2431506,2.5163455],dtype=float32)In the example shown, the first three lines import the required modules: modulespectrophorefor the calculation of spectrophores, moduleChemto generate a RDKit molecule from a smiles string, and moduleAllChemto generate a 3D-conformation from the molecule. Next, a molecule is created from a smiles string (line 4), and a conformation is then generated at the 6'th line after adding hydrogen atoms on the 5'th line. Finally, on lines 7 and 8, aSpectrophoreCalculatorobject is generated and this object is then used to calculate aspectrophoredescriptor (line 8), which consists in its default form of 4 * 12 numbers.Note: a few words on the shape of aspectrophore:Eachspectrophoreconsists of a set of floating point numbers, and this set is always a multiple of 4. The actual number count depends on how stereochemistry is treated in the calculation of spectrophores; this is controlled by thestereo()method:stereo("none"): the total number of numbers in aspectrophoreis 48 (4 * 12; default),stereo("unique"): the total number of numbers in aspectrophoreis 72 (4 * 18),stereo("mirror"): the total number of numbers in aspectrophoreis 72 (4 * 18),stereo("all"): the total number of numbers in aspectrophoreis 144 (4 * 36).Whatever the actual number ofspectrophorepoints, these are always calculated according the same atomic properties. For example, consider aspectrophoreof 4npoints, then these points represent the following:Points 1 ton: representing the interaction energies between theatomic partial chargesand each of thenboxes;Pointsn+1 to 2n: representing the interaction energies between theatomic lipophilicitiesand each of thenboxes;Points 2n+1 to 3n: representing the interaction energies between theatomic shape deviationsand each of thenboxes;Points 3n+1 to 4n: representing the interaction energies between theatomic electrophilicitiesand each of thenboxes.Please have a look at the original publication form more information about the way these interaction energies are calculated, and what thestereo()method actually means.If a molecule contains more than one 3D-conformation, then one may specify which conformation should be used for the calculation ofspectrophores. As an example, consider the following code:>>>calculator=spectrophore.SpectrophoreCalculator(normalization='none')Probesinitialised:48numberofprobesintotal12probesareusedduetotheimposedstereoflag>>>aspirin=Chem.MolFromSmiles("CC(Oc1ccccc1C(O)=O)=O")>>>mol=Chem.AddHs(mol)>>>cids=AllChem.EmbedMultipleConfs(aspirin,numConfs=3,randomSeed=1)>>>print(len(cids))3>>>foriinrange(len(cids)):calculator.calculate(aspirin,i)...array([2.964628,3.1078947,2.927014,4.7348037,7.507005,6.7752705,4.694607,4.9843326,6.566493,8.246073,10.165346,6.63523,4.858508,8.002102,6.8100824,8.816333,15.715073,19.571812,10.928973,14.395827,17.003227,18.447824,25.714355,15.146796,72.9549,98.34449,169.34996,182.39804,131.36954,131.15866,65.37012,130.0362,162.26236,149.89626,179.36638,198.5693,2.2463505,3.1564593,5.1663566,5.612588,4.058919,4.409714,2.2037854,4.4034805,4.9583206,5.239315,5.461795,6.264689],dtype=float32)array([2.863708,3.1190798,2.9663007,4.770968,7.393107,7.3158054,4.9012723,5.10262,6.548969,8.572092,10.425214,6.4823613,4.787042,8.08808,6.631177,8.741646,16.067795,19.49238,10.819519,14.260894,16.789541,18.33067,25.610632,14.279321,69.21315,96.67396,170.67822,184.54782,119.22876,135.03757,59.888947,119.49558,173.35124,145.16624,180.47777,187.60854,2.1962543,3.108443,5.2100787,5.6747303,3.8506951,4.435027,2.1061015,4.0988173,5.171605,5.103154,5.384299,5.955127],dtype=float32)array([3.0309825,3.435472,2.8768196,4.706544,7.5557814,6.4479575,4.55689,4.953575,6.4871607,8.706506,8.518427,6.3662963,4.9284625,9.355401,6.6179686,8.523829,15.459739,19.284777,10.792515,13.991817,16.795666,18.597605,24.084375,13.117221,77.639145,120.10927,169.49625,166.18648,131.14139,144.46242,72.4695,149.30933,140.35475,155.59204,130.84991,174.86932,2.4413445,3.8801153,5.1489463,4.834638,4.0795846,4.013626,2.4914305,4.5840054,4.270138,5.335861,4.6315002,5.6371183],dtype=float32)One can easily visualisespectrophoresby plotting the actual values. For example, consider the following snippet:>>>importmatplotlib.pyplotasplt>>>mol=Chem.MolFromSmiles("CC(CCC1=CC=CC=C1Cl)N1CCOCC1")>>>mol=Chem.AddHs(mol)>>>cids=AllChem.EmbedMultipleConfs(mol,numConfs=10,randomSeed=1)>>>spectrophores=[]>>>forcidincids:spectrophores.append(calculator.calculate(mol,cid))...>>>foriinrange(len(spectrophores)):plt.plot(range(1,49),spectrophores[i],label='Conf%d'%(i+1))...[<matplotlib.lines.Line2Dobjectat0x7f989dc3f4f0>][<matplotlib.lines.Line2Dobjectat0x7f989dc3f850>][<matplotlib.lines.Line2Dobjectat0x7f989dc3fbb0>][<matplotlib.lines.Line2Dobjectat0x7f989dc3ff10>][<matplotlib.lines.Line2Dobjectat0x7f989dc512b0>][<matplotlib.lines.Line2Dobjectat0x7f989dc51610>][<matplotlib.lines.Line2Dobjectat0x7f989dc51970>][<matplotlib.lines.Line2Dobjectat0x7f989dc51cd0>][<matplotlib.lines.Line2Dobjectat0x7f989dc5e070>][<matplotlib.lines.Line2Dobjectat0x7f989dc5e3d0>]>>>plt.legend(loc='upper left')<matplotlib.legend.Legendobjectat0x7f9899470e80>>>>plt.grid()>>>plt.savefig("spectrophore/images/exampleplot1.png")which generates the following plot:Similarly, one can easily compare thespectrophoresfrom two different molecules, and quantify the difference:>>>plt.close()>>>spectrophores=[]>>>mols=[Chem.MolFromSmiles("ClC(Br)(I)F"),Chem.MolFromSmiles("CC(CCC1=CC=CC=C1Cl)N1CCOCC1")]>>>foriinrange(2):...mols[i]=Chem.AddHs(mols[i])...AllChem.EmbedMolecule(mols[i],randomSeed=1)...spectrophores.append(calculator.calculate(mols[i]))...00>>>foriinrange(2):plt.plot(range(1,49),spectrophores[i],label='Molecule%d'%(i+1))...[<matplotlib.lines.Line2Dobjectat0x7faf420df520>][<matplotlib.lines.Line2Dobjectat0x7faf420df4c0>]>>>plt.grid()>>>plt.savefig("spectrophore/images/exampleplot2.png")>>>fromscipy.spatialimportdistance>>>distance.euclidean(spectrophores[0],spectrophores[1])2060.65478515625From the last example, it is clear that the actualspectrophorevalues may differ a lot depending on the type of molecule. Also, the absolute values are depending on the property type, with some properties leading to large values (e.g. shape deviation) and others very small. For this reason, a number of normalisation methods are provided as shown below.2.2. Module methodsresolution()Theresolution()method controls the smallest distance between the molecule and the surrounding box. By default this value is set to 3.0 A. Theresolution()can be specified at the moment of class creation, or later on using theresolution()method:>>>mol=Chem.MolFromSmiles("ClC(Br)(I)F")>>>AllChem.EmbedMolecule(mol,randomSeed=1)0>>>calculator=spectrophore.SpectrophoreCalculator(normalization='none')# Default of 3.0Probesinitialised:48numberofprobesintotal12probesareusedduetotheimposedstereoflag>>>print(calculator.calculate(mol)[0])2.9869986>>>calculator=spectrophore.SpectrophoreCalculator(normalization='none',resolution=3.0)Probesinitialised:48numberofprobesintotal12probesareusedduetotheimposedstereoflag>>>print(calculator.calculate(mol)[0])2.9869986>>>calculator=spectrophore.SpectrophoreCalculator(normalization='none',resolution=5.0)Probesinitialised:48numberofprobesintotal12probesareusedduetotheimposedstereoflag>>>print(calculator.calculate(mol)[0])1.341883>>>calculator.resolution(10.0)>>>print(calculator.calculate(mol)[0])0.3347178The larger the resolution value (e.g. 10.0versus3.0 A), the smaller the interaction energies and correspondingspectrophorevalues.Calling theresolution()method without an argument returns the current resolution value:>>>calculator.resolution()10.0accuracy()Theaccuracy()method controls the angular stepsize by which the molecule is rotated within the cages. By default this value is set to 20°. This parameter can be modified either at class creation, or using theaccuracy()method later on. The accuracy should be an integer fraction of 180, hence 180 modulusaccuracyshould be equal to 0. The smaller the accuracy value (meaning smaller angular stepsizes), the longer the computation time:>>>calculator=spectrophore.SpectrophoreCalculator(accuracy=20.0,normalization='none')# DefaultProbesinitialised:48numberofprobesintotal12probesareusedduetotheimposedstereoflag>>>print(calculator.calculate(mol)[0])2.9869986>>>calculator=spectrophore.SpectrophoreCalculator(normalization='none')Probesinitialised:48numberofprobesintotal12probesareusedduetotheimposedstereoflag>>>print(calculator.calculate(mol)[0])2.9869986>>>calculator=spectrophore.SpectrophoreCalculator(accuracy=2.0,normalization='none')Probesinitialised:48numberofprobesintotalOnlyusing12probes>>>print(calculator.calculate(mol)[0])# Takes some time3.0315504>>>100.0*(3.0315504-2.9869986)/3.03155041.469604463775361Calling theaccuracy()method without an argument returns the current accuracy value:>>>calculator.accuracy()2normalization()With thenormalization()method, one can specify the type ofspectrophorenormalization. There are four possibilities:normalization("none"): no normalization is applied and thespectrophorevalues are the raw calculated interaction energies (multiplied by -100),normalization("mean"): for each property, the average value is calculated and each of the individualspectrophoreproperty value are reduced by these mean values. This centers the calculated values around 0,normalization("std"): for each property, the standard deviation is calculated and each of the individualspectrophoreproperty value is divided by these standard deviations,normalization("all"): each spectrophore value is normalized by mean and standard deviation. This is the fefault option.The default value is "all".>>>calculator.accuracy(20)>>>calculator.normalization("none")>>>spec=calculator.calculate(mol)>>>print(spec[:12])[2.98699862.70232151.80297094.4689097.37554457.25227454.11233474.05599365.90845978.56496059.3282566.22969]>>>sum(spec[:12])64.78871297836304>>>calculator.normalization("mean")>>>spec=calculator.calculate(mol)>>>print(spec[:12])[-2.4120607-2.6967378-3.5960884-0.93015051.97648531.8532152-1.2867246-1.34306570.509400373.16590123.92919640.8306308]>>>sum(spec[:12])1.430511474609375e-06>>>calculator.normalization("std")>>>spec=calculator.calculate(mol)>>>print(spec[:12])[1.29241111.16923740.78010741.93360233.19124223.13790581.77932021.75494272.55646563.70587734.0361392.695455]>>>sum(spec[:12])28.032706141471863>>>calculator.normalization("all")>>>spec=calculator.calculate(mol)>>>print(spec[:12])[-1.0436476-1.1668214-1.5559514-0.402456460.855183360.80184704-0.5567385-0.581116140.220406771.36981841.70008020.35939637]>>>sum(spec[:12])6.854534149169922e-07Using a normalization over 'all' makes it more easier to comparespectrophoresbetween molecules:>>>plt.close()>>>mols=[Chem.MolFromSmiles("ClC(Br)(I)F"),Chem.MolFromSmiles("CC(CCC1=CC=CC=C1Cl)N1CCOCC1")]>>>spectrophores=[]>>>foriinrange(2):...mols[i]=Chem.AddHs(mols[i])...AllChem.EmbedMolecule(mols[i],randomSeed=1)...spectrophores.append(calculator.calculate(mols[i]))...00>>>foriinrange(2):plt.plot(range(1,49),spectrophores[i],label='Molecule%d'%(i+1))...[<matplotlib.lines.Line2Dobjectat0x7faf420df520>][<matplotlib.lines.Line2Dobjectat0x7faf420df4c0>]>>>plt.legend()<matplotlib.legend.Legendobjectat0x7f989ed85820>>>>plt.grid()>>>plt.savefig("spectrophore/images/exampleplot3.png")>>>fromscipy.spatialimportdistance>>>distance.euclidean(spectrophores[0],spectrophores[1])8.374645233154297The same holds true when comparingspectrophoresfrom different conformations:>>>plt.close()>>>spectrophores=[]>>>mol=Chem.MolFromSmiles("CC(CCC1=CC=CC=C1Cl)N1CCOCC1")>>>mol=Chem.AddHs(mol)>>>cids=AllChem.EmbedMultipleConfs(mol,numConfs=10,randomSeed=1)>>>calculator.normalization("all")>>>forcidincids:spectrophores.append(calculator.calculate(mol,cid))...>>>foriinrange(len(spectrophores)):plt.plot(range(1,49),spectrophores[i],label='Conf%d'%(i+1))...[<matplotlib.lines.Line2Dobjectat0x7f989f9cc070>][<matplotlib.lines.Line2Dobjectat0x7f989f9cc3d0>][<matplotlib.lines.Line2Dobjectat0x7f989f9cc730>][<matplotlib.lines.Line2Dobjectat0x7f989f9cca90>][<matplotlib.lines.Line2Dobjectat0x7f989f9ccdf0>][<matplotlib.lines.Line2Dobjectat0x7f989f9d8190>][<matplotlib.lines.Line2Dobjectat0x7f989f9d84f0>][<matplotlib.lines.Line2Dobjectat0x7f989f9d8850>][<matplotlib.lines.Line2Dobjectat0x7f989f9d8bb0>][<matplotlib.lines.Line2Dobjectat0x7f989f9d8f10>]>>>plt.legend(loc='upper left')<matplotlib.legend.Legendobjectat0x7f989f948e50>>>>plt.savefig("spectrophore/images/exampleplot4.png")>>>fromscipy.spatialimportdistance>>>distance.euclidean(spectrophores[0],spectrophores[1])5.974719524383545>>>distance.euclidean(spectrophores[0],spectrophores[2])6.081935882568359>>>distance.euclidean(spectrophores[1],spectrophores[2])3.7508902549743652stereo()Thestereo()method specifies the kind of cages to be used. The reason for this is that some of the cages that are used to calculatespectrophoreshave a stereospecific distribution of the interaction points:There are four possibilities:stereo("none"): no stereospecificity (default).Spectrophoresare generated using cages that are not stereospecific. For most applications, thesespectrophoreswill suffice,stereo("unique"): unique stereospecificity.Spectrophoresare generated using unique stereospecific cages,stereo("mirror"): mirror stereospecificity. Mirror stereospecificspectrophoresarespectrophoresresulting from the mirror enantiomeric form of the input molecules,stereo("all"): all cages are used. This results in the longestspectrophoresand should only in specific cases be used.The differences between the corresponding data points of unique and mirror stereospecificspectrophoresare very small and require very long calculation times to obtain a sufficiently high quality level. This increased quality level is triggered by theaccuracysetting and will result in calculation times being increased by at least a factor 100. As a consequence, it is recommended to apply this increased accuracy only in combination with a limited number of molecules, and when the small differences between the stereospecificspectrophoresare really critical. However, for the vast majority of virtual screening applications, this increased accuracy is not required as long as it is not the intention to draw conclusions about differences in the underlying molecular stereoselectivity. Non-stereospecificspectrophoreswill therefore suffice for most applications.3. InterpretingspectrophoresAspectrophoreis a vector of real number and has a certain length. The length depends on the usedstereomethod and the number of properties. The standard setting uses a set of non-stereospecific probes in combination with four properties:property 1: atomic partial chargesproperty 2: atomic lipophilicitiesproperty 3: atomic shape deviationsproperty 4: atomic electrophilictiesThe combination of four properties and the set of non-stereospecific probes leads to aspectrophorevector length of 48. The use of other probes leads to other vector lengths, as summarised in this table:StereospecificityNumber of probesNumber of propertiesLengthnone12448unique18472mirror18472all364144The general layout of aspectrophore, irrespective of its length, is always:Property 1Property 2Property 3Property 4probe 1..probenprobe 1..probenprobe 1..probenprobe 1..probenmeaning that the firstnvalues (withnbeing the number of probes) are calculated using property 1 (partial charges), then anothernvalues (n+1 up to 2n) calculated using property 2 (lipophilicities), and so forth.4. Release updates1.2.0:Switched to numpy.float32 type to achieve major speedup1.1.0:Updated and optimised the NumPy codeBug fixesIntroduced NumbaMade the 'all' normalization method the default oneAdded a test suite1.0.1: First official release on PyPi5. Reference and citationIf you use thespectrophoretechnology in your own research work, please cite as follows:Gladysz, R.; Mendes Dos Santos, F.; Langenaeker, W.;  Thijs, G.; Augustyns, K.; De Winter, H. (2018) 'Spectrophores as one-dimensional descriptors calculated from three-dimensional atomic properties: applications ranging from scaffold hopping to multi-target virtual screening',J. Cheminformatics10, 9.