From 229868cc918f00c4036dd3e28ad58ea675c5aec3 Mon Sep 17 00:00:00 2001 From: Aliaksandr Dziarkach <18146690+AliaksandrDziarkach@users.noreply.github.com> Date: Fri, 22 Sep 2023 13:55:02 +0300 Subject: [PATCH] #1282 SMARTS load/save errors in Indigo. (#1284) Co-authored-by: Aliaksandr Dziarkach --- api/c/tests/unit/tests/formats.cpp | 8 +- .../integration/common/rendering/__init__.py | 6 +- .../ref/basic/load_structure.py.out | 2 +- .../integration/ref/formats/smarts.py.out | 17 +++ api/tests/integration/test.py | 2 + api/tests/integration/tests/formats/smarts.py | 23 +++- .../tests/rendering/ref/linux/smarts/0190.png | Bin 5561 -> 5225 bytes .../tests/rendering/ref/mac/smarts/0190.png | Bin 6277 -> 5883 bytes .../tests/rendering/ref/win/smarts/0190.png | Bin 3156 -> 2997 bytes core/indigo-core/common/base_cpp/output.h | 17 ++- core/indigo-core/common/gzip/gzip_output.h | 2 +- core/indigo-core/molecule/base_molecule.h | 4 +- core/indigo-core/molecule/query_molecule.h | 5 + .../molecule/src/query_molecule.cpp | 25 +++- .../molecule/src/smiles_loader.cpp | 17 ++- .../indigo-core/molecule/src/smiles_saver.cpp | 116 +++++++++++++++--- 16 files changed, 200 insertions(+), 44 deletions(-) mode change 100644 => 100755 api/tests/integration/tests/formats/smarts.py diff --git a/api/c/tests/unit/tests/formats.cpp b/api/c/tests/unit/tests/formats.cpp index b9010de1b5..dcf05d45a7 100644 --- a/api/c/tests/unit/tests/formats.cpp +++ b/api/c/tests/unit/tests/formats.cpp @@ -60,14 +60,14 @@ TEST_F(IndigoApiFormatsTest, molecule) EXPECT_EQ(7, indigoCountBonds(obj)); // 3 - const string expectedSmarts = "[#6;A]1-[#6;A]=[#6;A]-[#6;A]=[#6;A]-[#6;A]=1-[!#1]"; + const string expectedSmarts = "[C]1-[C]=[C]-[C]=[C]-[C]=1-[*]"; obj = indigoLoadStructureFromString(mStr.c_str(), "smarts"); EXPECT_STREQ(expectedSmarts.c_str(), indigoSmarts(obj)); EXPECT_EQ(7, indigoCountAtoms(obj)); EXPECT_EQ(7, indigoCountBonds(obj)); // 4 - const string expectedQuery = "[#6]1-[#6]=[#6]-[#6]=[#6]-[#6]=1-[!#1]"; + const string expectedQuery = "[#6]1-[#6]=[#6]-[#6]=[#6]-[#6]=1-[*]"; obj = indigoLoadStructureFromString(mStr.c_str(), "query"); EXPECT_STREQ(expectedQuery.c_str(), indigoSmarts(obj)); EXPECT_EQ(7, indigoCountAtoms(obj)); @@ -83,13 +83,13 @@ TEST_F(IndigoApiFormatsTest, reaction) { const string react = "C1=C(*)C=CC=C1>>C1=CC=CC(*)=C1"; const string expected = "C1C=CC=CC=1*>>C1C=C(*)C=CC=1"; + const string expected_smarts = "[C]1-[C]=[C]-[C]=[C]-[C]=1-[*]>>[C]1-[C]=[C](-[*])-[C]=[C]-[C]=1"; try { int obj = -1; obj = indigoLoadStructureFromString(react.c_str(), "smarts"); - EXPECT_STREQ(expected.c_str(), indigoSmiles(obj)); - EXPECT_STREQ(expected.c_str(), indigoCanonicalSmiles(obj)); + EXPECT_STREQ(expected_smarts.c_str(), indigoSmarts(obj)); EXPECT_EQ(1, indigoCountReactants(obj)); EXPECT_EQ(1, indigoCountProducts(obj)); EXPECT_EQ(2, indigoCountMolecules(obj)); diff --git a/api/tests/integration/common/rendering/__init__.py b/api/tests/integration/common/rendering/__init__.py index 5b80150aea..a72ccd4065 100644 --- a/api/tests/integration/common/rendering/__init__.py +++ b/api/tests/integration/common/rendering/__init__.py @@ -1,3 +1,4 @@ +import base64 import os import platform import sys @@ -204,14 +205,17 @@ def checkBitmapSimilarity(filename, ref_filename): return "%s rendering status: Problem: %s" % (filename, str(e)) channels = ["red", "green", "blue", "alpha"] + with open("%s/out/%s" % (dirname, filename), "rb") as file: + binary_data = file.read() for i, result in enumerate(results): if result > (HASH_SIZE**2) * 0.1: return ( - "%s rendering status: Problem: PNG similarity is %s for %s channel" + "%s rendering status: Problem: PNG similarity is %s for %s channel\n%s\n" % ( filename, round(1 - (result / float(HASH_SIZE**2)), 2), channels[i], + base64.b64encode(binary_data), ) ) diff --git a/api/tests/integration/ref/basic/load_structure.py.out b/api/tests/integration/ref/basic/load_structure.py.out index 96fb38bb49..cd0a855d06 100644 --- a/api/tests/integration/ref/basic/load_structure.py.out +++ b/api/tests/integration/ref/basic/load_structure.py.out @@ -26,7 +26,7 @@ C1C=CC=CC=1*>>C1C=C(*)C=CC=1 C1C=CC=CC=1* |$;;;;;;A$| (** #8 **): Smarts, (smarts parameter): -[#8;A;H]-[#6;a]1-,:[#6;a]-,:[#6;a]-,:[#6;a]-,:[#6;a]-,:[#6;a]-,:1 +[O;H]-[c]1-,:[c]-,:[c]-,:[c]-,:[c]-,:[c]-,:1 (** #9 **): Smarts, (query parameter): [OH]C1:C:C:C:C:C:1 diff --git a/api/tests/integration/ref/formats/smarts.py.out b/api/tests/integration/ref/formats/smarts.py.out index c61ce65b05..8c9ad54b94 100644 --- a/api/tests/integration/ref/formats/smarts.py.out +++ b/api/tests/integration/ref/formats/smarts.py.out @@ -9,3 +9,20 @@ CC[C+5]CCCCC **** Load and Save as Query with component-level grouping **** ([#8].[#6]) is ok. smarts_in==smarts_out ([#8].[#6]).([#8].[#6]) is ok. smarts_in==smarts_out +[!C;!b] is ok. smarts_in==smarts_out +[*] is ok. smarts_in==smarts_out +[*;R1] is ok. smarts_in==smarts_out +[*;R3] is ok. smarts_in==smarts_out +[r] is ok. smarts_in==smarts_out +[r0] is ok. smarts_in==smarts_out +[r1] is ok. smarts_in==smarts_out +[r3] is ok. smarts_in==smarts_out +[v] is ok. smarts_in==smarts_out +[v0] is ok. smarts_in==smarts_out +[v3] is ok. smarts_in==smarts_out +[+0] is ok. smarts_in==smarts_out +[#6]@[#6] is ok. smarts_in==smarts_out +[#9]/[#6] is ok. smarts_in==smarts_out +[#9]/[#6]=[C]/[#17] is ok. smarts_in==smarts_out +[O;H] is ok. smarts_in==smarts_out +[!O;H] is ok. smarts_in==smarts_out diff --git a/api/tests/integration/test.py b/api/tests/integration/test.py index 66d765239f..0140c7b4a1 100644 --- a/api/tests/integration/test.py +++ b/api/tests/integration/test.py @@ -346,6 +346,8 @@ def run_analyze_test(args): f.close() with stdout_lock: sys.__stdout__.write(out_message + "\n") + if test_status == "[FAILED]": + sys.__stdout__.write(msg + "\n") test_result = (root, filename, test_status, msg, tspent) return test_result diff --git a/api/tests/integration/tests/formats/smarts.py b/api/tests/integration/tests/formats/smarts.py old mode 100644 new mode 100755 index 6d4c4b039a..b421fd6801 --- a/api/tests/integration/tests/formats/smarts.py +++ b/api/tests/integration/tests/formats/smarts.py @@ -1,3 +1,4 @@ +#!/bin/env python3 import os import sys @@ -23,8 +24,8 @@ def test_smarts_load_save(smarts_in): print("%s is ok. smarts_in==smarts_out" % smarts_in) else: print("smarts_in!=smarts_out") - print(" smarts_in=%s", smarts_in) - print("smarts_out=%s", smarts_out) + print(" smarts_in=%s" % smarts_in) + print("smarts_out=%s" % smarts_out) molstr = """ @@ -96,3 +97,21 @@ def test_smarts_load_save(smarts_in): print("**** Load and Save as Query with component-level grouping ****") test_smarts_load_save("([#8].[#6])") test_smarts_load_save("([#8].[#6]).([#8].[#6])") + +test_smarts_load_save("[!C;!b]") +test_smarts_load_save("[*]") +test_smarts_load_save("[*;R1]") +test_smarts_load_save("[*;R3]") +test_smarts_load_save("[r]") +test_smarts_load_save("[r0]") +test_smarts_load_save("[r1]") +test_smarts_load_save("[r3]") +test_smarts_load_save("[v]") +test_smarts_load_save("[v0]") +test_smarts_load_save("[v3]") +test_smarts_load_save("[+0]") +test_smarts_load_save("[#6]@[#6]") +test_smarts_load_save("[#9]/[#6]") +test_smarts_load_save("[#9]/[#6]=[C]/[#17]") +test_smarts_load_save("[O;H]") +test_smarts_load_save("[!O;H]") diff --git a/api/tests/integration/tests/rendering/ref/linux/smarts/0190.png b/api/tests/integration/tests/rendering/ref/linux/smarts/0190.png index 0e59d707d1f1fb3a31aef5b9d8c9e7fae8b2825c..cbece9861c30f06ac683ede780af2b34ae27989e 100644 GIT binary patch literal 5225 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 literal 5561 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

*Mp(f~qf$e$LL2@uM9Ue@NW6joS2I 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 arr); @@ -111,7 +110,7 @@ namespace indigo Array& _arr; }; - class DLLEXPORT StringOutput : public Output, public OutputTell + class DLLEXPORT StringOutput : public Output { public: StringOutput() = delete; @@ -127,7 +126,7 @@ namespace indigo std::string& _str; }; - class DLLEXPORT StandardOutput : public Output, public OutputTell + class DLLEXPORT StandardOutput : public Output { public: explicit StandardOutput(); diff --git a/core/indigo-core/common/gzip/gzip_output.h b/core/indigo-core/common/gzip/gzip_output.h index d5653fa79e..6101e531c1 100644 --- a/core/indigo-core/common/gzip/gzip_output.h +++ b/core/indigo-core/common/gzip/gzip_output.h @@ -27,7 +27,7 @@ namespace indigo { - class GZipOutput : public Output, OutputTell + class GZipOutput : public Output { public: enum diff --git a/core/indigo-core/molecule/base_molecule.h b/core/indigo-core/molecule/base_molecule.h index ef49bd4ea8..b7f19d68c5 100644 --- a/core/indigo-core/molecule/base_molecule.h +++ b/core/indigo-core/molecule/base_molecule.h @@ -69,7 +69,9 @@ namespace indigo _BOND_DOUBLE_OR_AROMATIC = 7, _BOND_ANY = 8, _BOND_COORDINATION = 9, - _BOND_HYDROGEN = 10 + _BOND_HYDROGEN = 10, + BOND_SMARTS_UP = 11, + BOND_SMARTS_DOWN = 12, }; enum diff --git a/core/indigo-core/molecule/query_molecule.h b/core/indigo-core/molecule/query_molecule.h index cd98fa75fd..b3f8c78ebf 100644 --- a/core/indigo-core/molecule/query_molecule.h +++ b/core/indigo-core/molecule/query_molecule.h @@ -97,6 +97,7 @@ namespace indigo ATOM_TEMPLATE_SEQID, ATOM_TEMPLATE_CLASS, ATOM_PI_BONDED, + ATOM_CHILARITY, BOND_ORDER, BOND_TOPOLOGY, @@ -117,6 +118,8 @@ namespace indigo // otherwise: no children PtrArray children; + bool artificial; // if true - added by parser to comply restrictions + // Check if node has any constraint of the specific type bool hasConstraint(int what_type); @@ -137,6 +140,8 @@ namespace indigo bool sureValueBelongs(int what_type, const int* arr, int count); bool sureValueBelongsInv(int what_type, const int* arr, int count); + bool hasOP_OR(); + // Optimize query for faster substructure search void optimize(); diff --git a/core/indigo-core/molecule/src/query_molecule.cpp b/core/indigo-core/molecule/src/query_molecule.cpp index 6a46a3175c..17ae527d92 100644 --- a/core/indigo-core/molecule/src/query_molecule.cpp +++ b/core/indigo-core/molecule/src/query_molecule.cpp @@ -506,7 +506,7 @@ bool QueryMolecule::isSaturatedAtom(int idx) throw Error("not implemented"); } -QueryMolecule::Node::Node(int type_) +QueryMolecule::Node::Node(int type_) : artificial(false) { type = (OpType)type_; } @@ -1186,6 +1186,29 @@ bool QueryMolecule::Node::sureValueBelongs(int what_type, const int* arr, int co } } +bool QueryMolecule::Node::hasOP_OR() +{ + int i; + + switch (type) + { + case OP_AND: { + for (i = 0; i < children.size(); i++) + if (children[i]->hasOP_OR()) + return true; + + return false; + } + case OP_OR: { + return true; + } + case OP_NOT: + return false; + default: + return false; + } +} + QueryMolecule::Atom* QueryMolecule::Atom::sureConstraint(int what_type) { int count = 0; diff --git a/core/indigo-core/molecule/src/smiles_loader.cpp b/core/indigo-core/molecule/src/smiles_loader.cpp index 71a519e3da..275f1fef7b 100644 --- a/core/indigo-core/molecule/src/smiles_loader.cpp +++ b/core/indigo-core/molecule/src/smiles_loader.cpp @@ -2148,8 +2148,9 @@ void SmilesLoader::_forbidHydrogens() std::unique_ptr newatom; std::unique_ptr oldatom(_qmol->releaseAtom(i)); - newatom.reset( - QueryMolecule::Atom::und(QueryMolecule::Atom::nicht(new QueryMolecule::Atom(QueryMolecule::ATOM_NUMBER, ELEM_H)), oldatom.release())); + std::unique_ptr notH(QueryMolecule::Atom::nicht(new QueryMolecule::Atom(QueryMolecule::ATOM_NUMBER, ELEM_H))); + notH->artificial = true; + newatom.reset(QueryMolecule::Atom::und(notH.release(), oldatom.release())); _qmol->resetAtom(i, newatom.release()); } @@ -2565,7 +2566,10 @@ void SmilesLoader::_readBondSub(Array& bond_str, _BondDesc& bond, std::uni else if (next == '/') { scanner.skip(1); - order = BOND_SINGLE; + if (smarts_mode) + order = BOND_SMARTS_UP; + else + order = BOND_SINGLE; if (bond.dir == 2) throw Error("Specificiation of both cis- and trans- bond restriction is not supported yet."); bond.dir = 1; @@ -2573,7 +2577,10 @@ void SmilesLoader::_readBondSub(Array& bond_str, _BondDesc& bond, std::uni else if (next == '\\') { scanner.skip(1); - order = BOND_SINGLE; + if (smarts_mode) + order = BOND_SMARTS_DOWN; + else + order = BOND_SINGLE; if (bond.dir == 1) throw Error("Specificiation of both cis- and trans- bond restriction is not supported yet."); bond.dir = 2; @@ -3301,6 +3308,8 @@ void SmilesLoader::_readAtom(Array& atom_str, bool first_in_brackets, _Ato if (isdigit(scanner.lookNext())) subatom = std::make_unique(QueryMolecule::ATOM_SMALLEST_RING_SIZE, scanner.readUnsigned()); + else if (smarts_mode) + subatom = std::make_unique(QueryMolecule::ATOM_SMALLEST_RING_SIZE, 1, 100); else subatom = std::make_unique(QueryMolecule::ATOM_RING_BONDS, 1, 100); } diff --git a/core/indigo-core/molecule/src/smiles_saver.cpp b/core/indigo-core/molecule/src/smiles_saver.cpp index 7a7413838f..efa5a69d3a 100644 --- a/core/indigo-core/molecule/src/smiles_saver.cpp +++ b/core/indigo-core/molecule/src/smiles_saver.cpp @@ -922,6 +922,31 @@ void SmilesSaver::_writeCharge(int charge) const _output.printf("-"); } +static void _write_num(indigo::Output& output, unsigned char ch, int num) +{ + output.writeChar(ch); + if (num != 1) + output.printf("%d", num); +} + +static void _write_num_if_set(indigo::Output& output, unsigned char ch, int min, int max) +{ + if (min == 1 && max == 100) + output.writeChar(ch); + else + { + output.printf("%c%d", ch, min); + } +} + +static void writeAnd(Output& _output, QueryMolecule::Node* node, bool has_or_parent) +{ + if (has_or_parent) + _output.writeChar('&'); + else if (node->hasOP_OR()) + _output.writeChar(';'); +} + void SmilesSaver::_writeSmartsAtom(int idx, QueryMolecule::Atom* atom, int chirality, int depth, bool has_or_parent, bool has_not_parent) const { int i; @@ -932,20 +957,65 @@ void SmilesSaver::_writeSmartsAtom(int idx, QueryMolecule::Atom* atom, int chira switch (atom->type) { case QueryMolecule::OP_NOT: { + if (atom->artificial) // Skip atoms added by loader (!#1) + { + break; + } + else if (QueryMolecule::isNotAtom(*atom, ELEM_H)) + { + _output.printf("*"); + break; + } _output.writeChar('!'); _writeSmartsAtom(idx, (QueryMolecule::Atom*)atom->children[0], chirality, depth + 1, has_or_parent, true); break; } case QueryMolecule::OP_AND: { + bool has_number = false; + bool has_aromatic = false; + bool aromatic = false; + char atom_name[10]; + int cur_pos = _output.tell(); + for (i = 0; i < atom->children.size(); i++) + { + if (atom->children[i]->type == QueryMolecule::ATOM_NUMBER) + { + has_number = true; + strncpy(atom_name, Element::toString(static_cast(atom->children[0])->value_max), sizeof(atom_name)); + } + if (atom->children[i]->type == QueryMolecule::ATOM_AROMATICITY) + { + has_aromatic = true; + aromatic = static_cast(atom->children[i])->value_min == ATOM_AROMATIC; + } + } + if (has_aromatic && has_number) + { // Convert a & #6 -> c, A & #6 -> C + if (aromatic) + atom_name[0] = tolower(atom_name[0]); + _output.printf("%s", atom_name); + } for (i = 0; i < atom->children.size(); i++) { + if (has_aromatic && has_number && + (atom->children[i]->type == QueryMolecule::ATOM_AROMATICITY || atom->children[i]->type == QueryMolecule::ATOM_NUMBER)) + { + continue; + } if (atom->children[i]->type == QueryMolecule::ATOM_RADICAL || atom->children[i]->type == QueryMolecule::ATOM_VALENCE) { continue; } + if (atom->children[i]->type == QueryMolecule::OP_NOT && atom->children[i]->artificial) + { + continue; + } - if (i > 0) + if (_output.tell() > cur_pos) + { _output.writeChar(has_or_parent ? '&' : ';'); + cur_pos = _output.tell(); + } _writeSmartsAtom(idx, (QueryMolecule::Atom*)atom->children[i], chirality, depth + 1, has_or_parent, has_not_parent); } break; @@ -1002,6 +1072,8 @@ void SmilesSaver::_writeSmartsAtom(int idx, QueryMolecule::Atom* atom, int chira _output.printf("+"); else if (charge == -1) _output.printf("-"); + else + _output.printf("+0"); break; } case QueryMolecule::ATOM_FRAGMENT: { @@ -1021,11 +1093,12 @@ void SmilesSaver::_writeSmartsAtom(int idx, QueryMolecule::Atom* atom, int chira _output.writeChar('*'); break; case QueryMolecule::ATOM_TOTAL_H: { - int hydro = atom->value_min; - if (hydro == 1) - _output.printf("H"); - else - _output.printf("H%d", hydro); + _write_num(_output, 'H', atom->value_min); + break; + } + + case QueryMolecule::ATOM_SSSR_RINGS: { + _write_num_if_set(_output, 'R', atom->value_min, atom->value_max); break; } @@ -1035,24 +1108,12 @@ void SmilesSaver::_writeSmartsAtom(int idx, QueryMolecule::Atom* atom, int chira } case QueryMolecule::ATOM_RING_BONDS: { - if (atom->value_min == 1 && atom->value_max == 100) - _output.printf("x"); - else - { - _output.printf("x%d", atom->value_min); - } + _write_num_if_set(_output, 'x', atom->value_min, atom->value_max); break; } case QueryMolecule::ATOM_IMPLICIT_H: { - if (atom->value_min == 1 && atom->value_max == 100) - { - _output.printf("h"); - } - else - { - _output.printf("h%d", atom->value_min); - } + _write_num_if_set(_output, 'h', atom->value_min, atom->value_max); break; } @@ -1062,6 +1123,7 @@ void SmilesSaver::_writeSmartsAtom(int idx, QueryMolecule::Atom* atom, int chira } case QueryMolecule::ATOM_SMALLEST_RING_SIZE: { + _write_num_if_set(_output, 'r', atom->value_min, atom->value_max); break; } @@ -1085,6 +1147,11 @@ void SmilesSaver::_writeSmartsAtom(int idx, QueryMolecule::Atom* atom, int chira break; } + case QueryMolecule::ATOM_TOTAL_BOND_ORDER: { + _write_num(_output, 'v', atom->value_min); + break; + } + default: { throw Error("Unknown atom attribute %d", atom->type); break; @@ -1146,6 +1213,15 @@ void SmilesSaver::_writeSmartsBond(int idx, QueryMolecule::Bond* bond, bool has_ _output.writeChar('#'); else if (bond_order == BOND_AROMATIC) _output.writeChar(':'); + else if (bond_order == BOND_SMARTS_UP) + _output.writeChar('/'); + else if (bond_order == BOND_SMARTS_DOWN) + _output.writeChar('\\'); + break; + } + case QueryMolecule::BOND_TOPOLOGY: { + if (bond->value == TOPOLOGY_RING) + _output.writeChar('@'); break; } default:;