diff --git a/.github/workflows/indigo-ci.yaml b/.github/workflows/indigo-ci.yaml index c0d24d1a2a..d3507ba247 100644 --- a/.github/workflows/indigo-ci.yaml +++ b/.github/workflows/indigo-ci.yaml @@ -1318,7 +1318,7 @@ jobs: path: utils/indigo-service/backend/lib/ - name: Build run: docker build -f ./utils/indigo-service/backend/Dockerfile -t epmlsop/indigo-service:latest ./utils/indigo-service/backend - - name: Test Imago + - name: Test Imago & Indigo run: | docker run --rm=true -d -p 8080:80 --name=indigo_service epmlsop/indigo-service:latest sleep 10 @@ -1326,6 +1326,7 @@ jobs: docker ps export INDIGO_SERVICE_URL=http://localhost:8080/v2 python3 utils/indigo-service/backend/service/tests/api/imago_test.py + python3 utils/indigo-service/backend/service/tests/api/indigo_test.py docker logs indigo_service docker stop indigo_service # TODO: add indigo tests diff --git a/README.md b/README.md index b85e8c9d28..8b819fe7c1 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,26 @@ Indigo/build>cmake --build . --config Release --target all or any of the following targets could be specified: --target { indigo-dotnet | indigo-java | indigo-python } Build results could be collected from Indigo/dist folder. +## Run tests ## + +Befo run any test you have to build and install indigo-python +1) Build indigo-python using '--target all' or '--target indigo=python'. + Package should be in 'build' directory, it will be named like 'epam.indigo-version-arch.whl' +3) Install package using pip `python -m pip uninstall epam.indigo -y ; python -m pip install dist/epam.indigo-version-arch.whl` + +Run integration test using `python api/tests/integration/test.py -t 1` for all test, or `python api/tests/integration/test.py -t 1 -p test_name` to run tests by mask `test_name`. + +To run backend API test: +1) Install epam-indigo +2) Install waitress `python pip install waitress` +3) Run backend service : + * `cd utils/indigo-service/backend/service` + * `cp v2/common/config.py .` + * `waitress-serve --listen="127.0.0.1:5000 [::1]:5000" app:app` you may use any port instead of 5000 +4) Run backend API test: + * set environment variable `export INDIGO_SERVICE_URL=http://localhost:5000/v2` (in powershell `$env:INDIGO_SERVICE_URL="http://localhost:5000/v2"`) + * run test `python utils/indigo-service/backend/service/tests/api/indigo_test.py` use `-k test_name` to run test by pattern. + ## How to build Indigo-WASM ## ### Build tools prerequisites ### @@ -124,7 +144,7 @@ Make sure it's running from path: >source ./emsdk_env.sh ``` -Note: On Windows, run `emsdk` instead of `./emsdk`, and `emsdk_env.bat` instead of source `./emsdk_env.sh`. +Note: On Windows, run `emsdk` instead of `./emsdk`, and `emsdk_env.bat` instead of source `./emsdk_env.sh`, use `cmd` instead of `powershell`. ### Get Indigo sources ### diff --git a/api/c/indigo/src/indigo.cpp b/api/c/indigo/src/indigo.cpp index a624be7ecd..66a0c08010 100644 --- a/api/c/indigo/src/indigo.cpp +++ b/api/c/indigo/src/indigo.cpp @@ -118,6 +118,7 @@ void Indigo::init() max_embeddings = 10000; layout_max_iterations = 0; + layout_preserve_existing = false; molfile_saving_skip_date = false; diff --git a/api/c/indigo/src/indigo_internal.h b/api/c/indigo/src/indigo_internal.h index 12b6a1c58b..f3cfb66e86 100644 --- a/api/c/indigo/src/indigo_internal.h +++ b/api/c/indigo/src/indigo_internal.h @@ -332,6 +332,7 @@ class DLLEXPORT Indigo int layout_max_iterations; // default is zero -- no limit bool smart_layout = false; float layout_horintervalfactor = ReactionLayout::DEFAULT_HOR_INTERVAL_FACTOR; + bool layout_preserve_existing = false; int layout_orientation = 0; diff --git a/api/c/indigo/src/indigo_layout.cpp b/api/c/indigo/src/indigo_layout.cpp index bfc4023eec..11fc2405d9 100644 --- a/api/c/indigo/src/indigo_layout.cpp +++ b/api/c/indigo/src/indigo_layout.cpp @@ -58,7 +58,7 @@ CEXPORT int indigoLayout(int object) ml.max_iterations = self.layout_max_iterations; ml.bond_length = MoleculeLayout::DEFAULT_BOND_LENGTH; ml.layout_orientation = (layout_orientation_value)self.layout_orientation; - if (mol->hasAtropoStereoBonds()) + if (self.layout_preserve_existing || mol->hasAtropoStereoBonds()) ml.respect_existing_layout = true; TimeoutCancellationHandler cancellation(self.cancellation_timeout); @@ -107,6 +107,8 @@ CEXPORT int indigoLayout(int object) rl.layout_orientation = (layout_orientation_value)self.layout_orientation; rl.bond_length = MoleculeLayout::DEFAULT_BOND_LENGTH; rl.horizontal_interval_factor = self.layout_horintervalfactor; + if (self.layout_preserve_existing) + rl.preserve_molecule_layout = true; rl.make(); try { diff --git a/api/c/indigo/src/indigo_options.cpp b/api/c/indigo/src/indigo_options.cpp index 774dddc820..3484b187e1 100644 --- a/api/c/indigo/src/indigo_options.cpp +++ b/api/c/indigo/src/indigo_options.cpp @@ -310,7 +310,7 @@ void IndigoOptionHandlerSetter::setBasicOptionHandlers(const qword id) mgr->setOptionHandlerInt("max-embeddings", indigoSetMaxEmbeddings, indigoGetMaxEmbeddings); mgr->setOptionHandlerInt("layout-max-iterations", SETTER_GETTER_INT_OPTION(indigo.layout_max_iterations)); - + mgr->setOptionHandlerInt("layout-preserve-existing", SETTER_GETTER_BOOL_OPTION(indigo.layout_preserve_existing)); mgr->setOptionHandlerFloat("layout-horintervalfactor", indigoSetLayoutHorIntervalFactor, indigoGetLayoutHorIntervalFactor); mgr->setOptionHandlerInt("aam-timeout", SETTER_GETTER_INT_OPTION(indigo.aam_cancellation_timeout)); diff --git a/api/python/indigo/indigo/indigo.py b/api/python/indigo/indigo/indigo.py index ee0a9032f1..b3f8befedb 100644 --- a/api/python/indigo/indigo/indigo.py +++ b/api/python/indigo/indigo/indigo.py @@ -129,68 +129,41 @@ def setOption(self, option, value1, value2=None, value3=None): IndigoException: if option does not exist """ + opt = option.encode() if ( - ( - type(value1).__name__ == "str" - or type(value1).__name__ == "unicode" - ) - and value2 is None - and value3 is None - ): - IndigoLib.checkResult( - self._lib().indigoSetOption( - option.encode(), - value1.encode(), - ) - ) - elif ( - type(value1).__name__ == "int" - and value2 is None - and value3 is None + isinstance(value1, float) + and isinstance(value2, float) + and isinstance(value3, float) ): IndigoLib.checkResult( - self._lib().indigoSetOptionInt(option.encode(), value1) + self._lib().indigoSetOptionColor(opt, value1, value2, value3) ) elif ( - type(value1).__name__ == "float" - and value2 is None + isinstance(value1, int) + and isinstance(value2, int) and value3 is None ): IndigoLib.checkResult( - self._lib().indigoSetOptionFloat(option.encode(), value1) - ) - elif ( - type(value1).__name__ == "bool" - and value2 is None - and value3 is None - ): - value1_b = 0 - if value1: - value1_b = 1 - IndigoLib.checkResult( - self._lib().indigoSetOptionBool(option.encode(), value1_b) - ) - elif ( - type(value1).__name__ == "int" - and value2 - and type(value2).__name__ == "int" - and value3 is None - ): - IndigoLib.checkResult( - self._lib().indigoSetOptionXY(option.encode(), value1, value2) - ) - elif ( - type(value1).__name__ == "float" - and value2 - and type(value2).__name__ == "float" - and value3 - and type(value3).__name__ == "float" - ): - IndigoLib.checkResult( - self._lib().indigoSetOptionColor( - option.encode(), value1, value2, value3 - ) + self._lib().indigoSetOptionXY(opt, value1, value2) ) + elif value2 is None and value3 is None: + if isinstance(value1, str): + setOpt = self._lib().indigoSetOption + value = value1.encode() + elif isinstance(value1, int): + setOpt = self._lib().indigoSetOptionInt + value = value1 + elif isinstance(value1, float): + setOpt = self._lib().indigoSetOptionFloat + value = value1 + elif isinstance(value1, bool): + value1 = 0 + if value1: + value1 = 1 + setOpt = self._lib().indigoSetOptionBool + else: + raise IndigoException("bad option") + IndigoLib.checkResult(setOpt(opt, value)) else: raise IndigoException("bad option") diff --git a/api/python/indigo/indigo/indigo_object.py b/api/python/indigo/indigo/indigo_object.py index da03b6d5a2..5a888f61f5 100644 --- a/api/python/indigo/indigo/indigo_object.py +++ b/api/python/indigo/indigo/indigo_object.py @@ -3373,7 +3373,6 @@ def layout(self): Returns: int: 1 if there are no errors """ - return IndigoLib.checkResult(self._lib().indigoLayout(self.id)) def smiles(self): diff --git a/api/wasm/indigo-ketcher/indigo-ketcher.cpp b/api/wasm/indigo-ketcher/indigo-ketcher.cpp index 1b35bd8248..6c8c0515e6 100644 --- a/api/wasm/indigo-ketcher/indigo-ketcher.cpp +++ b/api/wasm/indigo-ketcher/indigo-ketcher.cpp @@ -411,6 +411,54 @@ namespace indigo return iko.toString(options, outputFormat.size() ? outputFormat : "ket"); } + std::string convert_explicit_hydrogens(const std::string& data, const std::string& mode, const std::string& outputFormat, + const std::map& options) + { + const IndigoSession session; + indigoSetOptions(options); + std::map options_copy = options; + if (outputFormat.find("smarts") != std::string::npos) + { + options_copy["query"] = "true"; + } + IndigoKetcherObject iko = loadMoleculeOrReaction(data, options_copy); + bool fold = false; + if (mode == "fold") + { + fold = true; + } + else if (mode == "unfold") + { + fold = false; + } + else if (mode == "auto") + { + IndigoObject iatoms(_checkResult(indigoIterateAtoms(iko.id()))); + while (_checkResult(indigoHasNext(iatoms.id))) + { + IndigoObject atom(_checkResult(indigoNext(iatoms.id))); + // indigoAtomicNumber can return -1 for non-standard atoms + // just skip these atoms + if (indigoAtomicNumber(atom.id) == 1) // hydrogen + { + fold = true; + break; + } + } + } + if (fold) + { + _checkResult(indigoFoldHydrogens(iko.id())); + } + else + { + indigoSetOptionBool("layout-preserve-existing", true); + _checkResult(indigoUnfoldHydrogens(iko.id())); + indigoSetOptionBool("layout-preserve-existing", false); + } + return iko.toString(options, outputFormat.size() ? outputFormat : "ket"); + } + std::string aromatize(const std::string& data, const std::string& outputFormat, const std::map& options) { const IndigoSession session; @@ -842,6 +890,7 @@ namespace indigo emscripten::function("version", &version); emscripten::function("versionInfo", &versionInfo); emscripten::function("convert", &convert); + emscripten::function("convert_explicit_hydrogens", &convert_explicit_hydrogens); emscripten::function("aromatize", &aromatize); emscripten::function("dearomatize", &dearomatize); emscripten::function("layout", &layout); diff --git a/api/wasm/indigo-ketcher/test/test.js b/api/wasm/indigo-ketcher/test/test.js index a99c83bc22..0daf96c646 100644 --- a/api/wasm/indigo-ketcher/test/test.js +++ b/api/wasm/indigo-ketcher/test/test.js @@ -531,6 +531,34 @@ M END } + // Convert explicit hydrogens + { + test("convert_explicit_hydrogens", "auto", () => { + let options = new indigo.MapStringString(); + options.set("output-content-type", "application/json"); + const unfold_smiles = indigo.convert_explicit_hydrogens("CC", "auto", "smiles", options); + assert.equal(unfold_smiles, '{"struct":"C([H])([H])([H])C([H])([H])[H]","format":"smiles","original_format":"chemical/x-daylight-smiles"}'); + const fold_smiles = indigo.convert_explicit_hydrogens("C([H])([H])([H])C([H])([H])[H]", "auto", "smiles", options); + assert.equal(fold_smiles, '{"struct":"CC","format":"smiles","original_format":"chemical/x-daylight-smiles"}'); + options.delete(); + }); + + test("convert_explicit_hydrogens", "fold", () => { + let options = new indigo.MapStringString(); + options.set("output-content-type", "application/json"); + const fold_smiles = indigo.convert_explicit_hydrogens("C([H])([H])([H])C([H])([H])[H]", "fold", "smiles", options); + assert.equal(fold_smiles, '{"struct":"CC","format":"smiles","original_format":"chemical/x-daylight-smiles"}'); + options.delete(); + }); + + test("convert_explicit_hydrogens", "unfold", () => { + let options = new indigo.MapStringString(); + options.set("output-content-type", "application/json"); + const unfold_smiles = indigo.convert_explicit_hydrogens("CC", "unfold", "smiles", options); + assert.equal(unfold_smiles, '{"struct":"C([H])([H])([H])C([H])([H])[H]","format":"smiles","original_format":"chemical/x-daylight-smiles"}'); + options.delete(); + }); + } // Dearomatize { diff --git a/utils/indigo-service/backend/service/tests/api/indigo_test.py b/utils/indigo-service/backend/service/tests/api/indigo_test.py index 5f2efc74cb..afcd72b948 100644 --- a/utils/indigo-service/backend/service/tests/api/indigo_test.py +++ b/utils/indigo-service/backend/service/tests/api/indigo_test.py @@ -468,8 +468,8 @@ def test_headers_wrong(self): self.assertEqual(400, result.status_code) expected_text = "ValidationError: {'input_format': ['Must be one of: chemical/x-mdl-rxnfile, \ chemical/x-mdl-molfile, chemical/x-indigo-ket, chemical/x-daylight-smiles, \ -chemical/x-cml, chemical/x-inchi, chemical/x-iupac, chemical/x-daylight-smarts, \ -chemical/x-inchi-aux, chemical/x-chemaxon-cxsmiles, chemical/x-cdxml.']}" +chemical/x-cml, chemical/x-inchi, chemical/x-inchi-key, chemical/x-iupac, chemical/x-daylight-smarts, \ +chemical/x-inchi-aux, chemical/x-chemaxon-cxsmiles, chemical/x-cdxml, chemical/x-cdx, chemical/x-sdf.']}" self.assertEquals( expected_text, result.text, @@ -486,8 +486,8 @@ def test_headers_wrong(self): self.assertEqual(400, result.status_code) expected_text = "ValidationError: {'output_format': ['Must be one of: chemical/x-mdl-rxnfile, \ chemical/x-mdl-molfile, chemical/x-indigo-ket, chemical/x-daylight-smiles, \ -chemical/x-cml, chemical/x-inchi, chemical/x-iupac, chemical/x-daylight-smarts, \ -chemical/x-inchi-aux, chemical/x-chemaxon-cxsmiles, chemical/x-cdxml.']}" +chemical/x-cml, chemical/x-inchi, chemical/x-inchi-key, chemical/x-iupac, chemical/x-daylight-smarts, \ +chemical/x-inchi-aux, chemical/x-chemaxon-cxsmiles, chemical/x-cdxml, chemical/x-cdx, chemical/x-sdf.']}" self.assertEquals( expected_text, result.text, @@ -645,10 +645,10 @@ def test_convert_canonical_smiles(self): def test_convert_smarts(self): smarts = [ - "[#8;A]-[!#1]-[#6;A](-[#9])(-[#9])-[#9]", + "[O]-[*]-[C](-[#9])(-[#9])-[#9]", "[#6,#1]", "[#1,#1]", - "[#9,#17,#35,#53,#7&A&+,$([OH]-*=[!#6]),+;!#1]", + "[#9,#17,#35,#53,N&+,$([OH]-*=[!#6]),+;*]", ] results = [] # results_get = [] @@ -935,12 +935,12 @@ def test_layout_selective_reaction(self): { "struct": """$RXN - + -INDIGO- 0100000000 2 1 0 $MOL - Ketcher 10071615322D 1 1.00000 0.00000 0 + -INDIGO-01000000002D 6 6 0 0 0 999 V2000 0.5450 0.6292 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 @@ -958,7 +958,7 @@ def test_layout_selective_reaction(self): M END $MOL - Ketcher 10071615322D 1 1.00000 0.00000 0 + -INDIGO-01000000002D 12 13 0 0 0 999 V2000 3.0898 -0.0001 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 @@ -989,7 +989,7 @@ def test_layout_selective_reaction(self): M END $MOL - Ketcher 10071615322D 1 1.00000 0.00000 0 + -INDIGO-01000000002D 6 6 0 0 0 999 V2000 16.4754 0.9017 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 @@ -1008,7 +1008,10 @@ def test_layout_selective_reaction(self): """, "selected": [5, 6], "output_format": "chemical/x-mdl-rxnfile", - "options": {"molfile-saving-skip-date": "1"}, + "options": { + "molfile-saving-skip-date": "1", + "molfile-saving-mode": "2000", + }, } ) result = requests.post( @@ -1027,12 +1030,12 @@ def test_layout_selective_reaction(self): -INDIGO-01000000002D 6 6 0 0 0 0 0 0 0 0999 V2000 - 1.3856 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 0.0000 -0.8000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 0.0000 -2.4000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 1.3856 -3.2000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 2.7713 -2.4000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 2.7713 -0.8000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.6856 1.6000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.3000 0.8000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1.3000 -0.8000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.6856 -1.6000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 4.0713 -0.8000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 4.0713 0.8000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 1 2 1 0 0 0 0 2 3 2 0 0 0 0 3 4 1 0 0 0 0 @@ -1045,18 +1048,18 @@ def test_layout_selective_reaction(self): -INDIGO-01000000002D 12 13 0 0 0 0 0 0 0 0999 V2000 - 8.2513 -1.6000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 9.0513 -0.2144 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 10.6513 -0.2144 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 11.4513 -1.6000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 10.6513 -2.9856 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 9.0513 -2.9856 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 13.0513 -1.6000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 13.8513 -0.2144 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 15.4513 -0.2144 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 16.2513 -1.6000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 15.4513 -2.9856 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 13.8513 -2.9856 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 7.6713 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 8.4713 1.3856 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 10.0713 1.3856 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 10.8713 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 10.0713 -1.3856 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 8.4713 -1.3856 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 12.4713 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 13.2713 1.3856 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 14.8713 1.3856 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 15.6713 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 14.8713 -1.3856 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 13.2713 -1.3856 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 1 2 1 0 0 0 0 2 3 2 0 0 0 0 3 4 1 0 0 0 0 @@ -1076,12 +1079,12 @@ def test_layout_selective_reaction(self): -INDIGO-01000000002D 6 6 0 0 0 0 0 0 0 0999 V2000 - 23.1169 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 21.7313 -0.8000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 21.7313 -2.4000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 23.1169 -3.2000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 24.5026 -2.4000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 - 24.5026 -0.8000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 23.2569 1.6000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 21.8713 0.8000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 21.8713 -0.8000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 23.2569 -1.6000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 24.6426 -0.8000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 24.6426 0.8000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 1 2 1 0 0 0 0 2 3 2 0 0 0 0 3 4 1 0 0 0 0 @@ -1253,13 +1256,9 @@ def test_automap_molecule_instead_of_reaction(self): ) def test_calculate_cip_correct(self): - result = requests.post( - self.url_prefix + "/calculate_cip", - headers={ - "Content-Type": "chemical/x-mdl-molfile", - "Accept": "chemical/x-mdl-molfile", - }, - data=""" + headers, data = self.get_headers( + { + "struct": """ Ketcher 07261618302D 1 1.00000 0.00000 0 12 12 0 0 0 999 V2000 @@ -1289,9 +1288,19 @@ def test_calculate_cip_correct(self): 3 12 1 0 0 0 M END """, + "input_format": "chemical/x-mdl-molfile", + "output_format": "chemical/x-mdl-molfile", + "options": {"molfile-saving-mode": "2000"}, + } + ) + result = requests.post( + self.url_prefix + "/calculate_cip", + headers=headers, + data=data, ) self.assertEqual(200, result.status_code) - # print(result.text) + res = json.loads(result.text) + # print(res["struct"]) self.assertEqual( """ 12 12 0 0 0 0 0 0 0 0999 V2000 @@ -1330,7 +1339,7 @@ def test_calculate_cip_correct(self): M SDD 2 0.0000 0.0000 DR ALL 1 1 M SED 2 (R) M END""", - "\n".join([s.rstrip() for s in result.text.splitlines()[2:]]), + "\n".join([s.rstrip() for s in res["struct"].splitlines()[2:]]), ) def test_render(self): @@ -2937,6 +2946,75 @@ def test_convert_cdxml(self): self.assertEqual("chemical/x-cdxml", result_data["format"]) self.assertIn("CDXML", result_data["struct"]) + def test_convert_explicit_hydrogens_auto(self): + params = { + "struct": "CC", + "mode": "auto", + "output_format": "chemical/x-daylight-smiles", + "input_format": "chemical/x-daylight-smiles", + } + headers, data = self.get_headers(params) + result = requests.post( + self.url_prefix + "/convert_explicit_hydrogens", + headers=headers, + data=data, + ) + self.assertEqual(200, result.status_code) + result_data = json.loads(result.text) + self.assertEqual( + "C([H])([H])([H])C([H])([H])[H]", result_data["struct"] + ) + params = { + "struct": result_data["struct"], + "output_format": "chemical/x-daylight-smiles", + "input_format": "chemical/x-daylight-smiles", + } + headers, data = self.get_headers(params) + result = requests.post( + self.url_prefix + "/convert_explicit_hydrogens", + headers=headers, + data=data, + ) + self.assertEqual(200, result.status_code) + result_data = json.loads(result.text) + self.assertEqual("CC", result_data["struct"]) + + def test_convert_explicit_hydrogens_fold(self): + params = { + "struct": "C([H])([H])([H])C([H])([H])[H]", + "mode": "fold", + "output_format": "chemical/x-daylight-smiles", + "input_format": "chemical/x-daylight-smiles", + } + headers, data = self.get_headers(params) + result = requests.post( + self.url_prefix + "/convert_explicit_hydrogens", + headers=headers, + data=data, + ) + self.assertEqual(200, result.status_code) + result_data = json.loads(result.text) + self.assertEqual("CC", result_data["struct"]) + + def test_convert_explicit_hydrogens_unfold(self): + params = { + "struct": "CC", + "mode": "unfold", + "output_format": "chemical/x-daylight-smiles", + "input_format": "chemical/x-daylight-smiles", + } + headers, data = self.get_headers(params) + result = requests.post( + self.url_prefix + "/convert_explicit_hydrogens", + headers=headers, + data=data, + ) + self.assertEqual(200, result.status_code) + result_data = json.loads(result.text) + self.assertEqual( + "C([H])([H])([H])C([H])([H])[H]", result_data["struct"] + ) + if __name__ == "__main__": unittest.main(verbosity=2, warnings="ignore") diff --git a/utils/indigo-service/backend/service/v2/indigo_api.py b/utils/indigo-service/backend/service/v2/indigo_api.py index eb4791157b..8e7649f700 100644 --- a/utils/indigo-service/backend/service/v2/indigo_api.py +++ b/utils/indigo-service/backend/service/v2/indigo_api.py @@ -21,6 +21,7 @@ IndigoAutomapSchema, IndigoCalculateSchema, IndigoCheckSchema, + IndigoConvertExplicitHydrogensSchema, IndigoRendererSchema, IndigoRequestSchema, ) @@ -204,7 +205,7 @@ def do_calc(m, func_name, precision): value = getattr(m, func_name)() except IndigoException as e: value = "calculation error: {0}".format(e.value.split(": ")[-1]) - if type(value) == float: + if isinstance(value, float): value = round(value, precision) return str(value) @@ -888,6 +889,119 @@ def convert(): ) +@indigo_api.route("/convert_explicit_hydrogens", methods=["POST"]) +@check_exceptions +def convert_explicit_hydrogens(): + """ + Convert hydrogens from implicit to explicit and vice versa + --- + tags: + - indigo + parameters: + - name: json_request + in: body + required: true + schema: + id: IndigoConvertExplicitHydrogensRequest + properties: + struct: + type: string + required: true + examples: C1=CC=CC=C1 + mode: + type: string + default: auto + enum: + auto + fold + unfold + output_format: + type: string + default: chemical/x-mdl-molfile + enum: + - chemical/x-mdl-rxnfile + - chemical/x-mdl-molfile + - chemical/x-indigo-ket + - chemical/x-daylight-smiles + - chemical/x-chemaxon-cxsmiles + - chemical/x-cml + - chemical/x-inchi + - chemical/x-iupac + - chemical/x-daylight-smarts + - chemical/x-inchi-aux + example: + struct: C1=CC=CC=C1 + output_format: chemical/x-mdl-molfile + responses: + 200: + description: Chemical structure with converted explicit hydrogens + schema: + $ref: "#/definitions/IndigoResponse" + 400: + description: 'A problem with supplied client data' + schema: + $ref: "#/definitions/ClientError" + 500: + description: 'A problem on server side' + schema: + $ref: "#/definitions/ServerError" + """ + data = IndigoConvertExplicitHydrogensSchema().load( + get_request_data(request) + ) + + LOG_DATA( + "[REQUEST] /convert_explicit_hydrogens", + data["input_format"], + data["output_format"], + data["struct"].encode("utf-8"), + data.get("mode", "mode undefined"), + ) + indigo = indigo_init(data["options"]) + query = False + if "smarts" in data["output_format"]: + query = True + md = load_moldata( + data["struct"], + mime_type=data["input_format"], + options=data["options"], + indigo=indigo, + query=query, + ) + fold = False + mode = data.get("mode", "auto") + if mode == "fold": + fold = True + elif mode == "unfold": + fold = False + else: + iatoms = md.struct.iterateAtoms() + while iatoms.hasNext(): + atom = iatoms.next() + try: + if atom.atomicNumber() == 1: # Hydrogen + fold = True + break + except IndigoException: + # atom.atomicNumber can raise exception for non-standard atoms + # just skip these atoms + continue + if fold: + md.struct.foldHydrogens() + else: + md.struct.unfoldHydrogens() + indigo.setOption("layout-preserve-existing", True) + md.struct.layout() + indigo.setOption("layout-preserve-existing", False) + return get_response( + md, + data["output_format"], + data["json_output"], + data["options"], + indigo=indigo, + ) + + @indigo_api.route("/layout", methods=["POST"]) @check_exceptions def layout(): diff --git a/utils/indigo-service/backend/service/v2/validation.py b/utils/indigo-service/backend/service/v2/validation.py index dd5b0194e1..078f2b15f4 100644 --- a/utils/indigo-service/backend/service/v2/validation.py +++ b/utils/indigo-service/backend/service/v2/validation.py @@ -168,6 +168,13 @@ class IndigoAutomapSchema(IndigoRequestSchema): ) +class IndigoConvertExplicitHydrogensSchema(IndigoRequestSchema): + mode = fields.Str( + missing="auto", + validate=OneOf(("auto", "fold", "unfold")), + ) + + class SearcherSchema(Schema): type = fields.Str( load_from="type",