diff --git a/modules/io/pymod/CMakeLists.txt b/modules/io/pymod/CMakeLists.txt index ead77d54b9d4cc0285845a166a52287774d73f67..02b392754d6b44ec9948b621ef86903ebf3ce060 100644 --- a/modules/io/pymod/CMakeLists.txt +++ b/modules/io/pymod/CMakeLists.txt @@ -4,6 +4,7 @@ set(OST_IO_PYMOD_SOURCES export_mmcif_io.cc export_omf_io.cc export_map_io.cc + export_sdf_io.cc ) set(OST_IO_PYMOD_MODULES diff --git a/modules/io/pymod/__init__.py b/modules/io/pymod/__init__.py index ea3be23a85b688a37946a55d298681aa5472979d..dca962c0bcd74611721a5e439e28d6eb1511f2d0 100644 --- a/modules/io/pymod/__init__.py +++ b/modules/io/pymod/__init__.py @@ -556,3 +556,46 @@ def _PDBize(biounit, asu, seqres=None, min_polymer_size=None, return pdb_bu MMCifInfoBioUnit.PDBize = _PDBize + + +def LoadSDF(filename, fault_tolerant=None, profile='DEFAULT'): + """ + Load SDF file from disk and return an entity. + + :param filename: File to be loaded + :type filename: :class:`str` + + :param fault_tolerant: Enable/disable fault-tolerant import. If set, overrides + the value of :attr:`IOProfile.fault_tolerant`. + :type fault_tolerant: :class:`bool` + + :param profile: Aggregation of flags and algorithms to control import and + processing of molecular structures. Can either be a + :class:`str` specifying one of the default profiles + ['DEFAULT', 'SLOPPY', 'CHARMM', 'STRICT'] or an actual object + of type :class:`ost.io.IOProfile`. + See :doc:`profile` for more info. + :type profile: :class:`str`/:class:`ost.io.IOProfile` + + :raises: :exc:`~ost.io.IOException` if the import fails due to an erroneous or + inexistent file + """ + def _override(val1, val2): + if val2 != None: + return val2 + else: + return val1 + + if isinstance(profile, str): + prof = profiles.Get(profile) + elif isinstance(profile, IOProfile): + prof = profile.Copy() + else: + raise TypeError('profile must be of type string or IOProfile, ' + \ + 'instead of %s' % type(profile)) + prof.fault_tolerant = _override(prof.fault_tolerant, fault_tolerant) + + reader = SDFReader(filename, prof) + ent = mol.CreateEntity() + reader.Import(ent) + return ent diff --git a/modules/io/pymod/export_sdf_io.cc b/modules/io/pymod/export_sdf_io.cc new file mode 100644 index 0000000000000000000000000000000000000000..08de790e2850229a568920c790767992491c075c --- /dev/null +++ b/modules/io/pymod/export_sdf_io.cc @@ -0,0 +1,64 @@ +//------------------------------------------------------------------------------ +// This file is part of the OpenStructure project <www.openstructure.org> +// +// Copyright (C) 2008-2020 by the OpenStructure authors +// +// This library is free software; you can redistribute it and/or modify it under +// the terms of the GNU Lesser General Public License as published by the Free +// Software Foundation; either version 3.0 of the License, or (at your option) +// any later version. +// This library is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +// details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with this library; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +//------------------------------------------------------------------------------ +#include <boost/python.hpp> +#include <boost/shared_ptr.hpp> +#include <boost/python/suite/indexing/vector_indexing_suite.hpp> +#include <boost/python/suite/indexing/map_indexing_suite.hpp> +using namespace boost::python; + +#include <ost/export_helper/pair_to_tuple_conv.hh> +#include <ost/io/mol/entity_io_sdf_handler.hh> +#include <ost/io/mol/io_profile.hh> +#include <ost/io/mol/sdf_reader.hh> +#include <ost/io/mol/sdf_writer.hh> +#include <ost/io/sdf_str.hh> +using namespace ost; +using namespace ost::io; +using namespace ost::mol; + +String (*sdf_str_a)(const mol::EntityHandle&)=&EntityToSDFString; +String (*sdf_str_b)(const mol::EntityView&)=&EntityToSDFString; + +void (*save_sdf_handle)(const mol::EntityHandle& entity, const String& filename)=&SaveSDF; +void (*save_sdf_view)(const mol::EntityView& entity, const String& filename)=&SaveSDF; + +void (SDFWriter::*write_handle)(const mol::EntityHandle&)=&SDFWriter::Write; +void (SDFWriter::*write_view)(const mol::EntityView&)=&SDFWriter::Write; + +void export_sdf_io() +{ + class_<SDFReader, boost::noncopyable>("SDFReader", init<String, const IOProfile&>()) + .def("Import", &SDFReader::Import) + ; + + class_<SDFWriter, boost::noncopyable>("SDFWriter", init<String>()) + .def("Write", write_handle) + .def("Write", write_view) + ; + + // def("LoadSDF", &LoadSDF); + def("SaveSDF", save_sdf_view); + def("SaveSDF", save_sdf_handle); + + def("EntityToSDFStr", sdf_str_a); + def("EntityToSDFStr", sdf_str_b); + + def("SDFStrToEntity", &SDFStringToEntity, (arg("SDF_string"), + arg("profile")=IOProfile())); +} diff --git a/modules/io/pymod/wrap_io.cc b/modules/io/pymod/wrap_io.cc index bae73b1e0fd9168c7673fc39da52ce678d6878d9..b7cade00c33e39eefd2ecc982b7e2c4e2d5a2a6f 100644 --- a/modules/io/pymod/wrap_io.cc +++ b/modules/io/pymod/wrap_io.cc @@ -30,9 +30,7 @@ using namespace boost::python; #include <ost/io/mol/entity_io_crd_handler.hh> #include <ost/io/mol/entity_io_pqr_handler.hh> #include <ost/io/mol/entity_io_mae_handler.hh> -#include <ost/io/mol/entity_io_sdf_handler.hh> #include <ost/io/mol/pdb_reader.hh> -#include <ost/io/mol/sdf_str.hh> #include <ost/io/mol/dcd_io.hh> #include <ost/io/stereochemical_params_reader.hh> using namespace ost; @@ -66,18 +64,13 @@ BOOST_PYTHON_FUNCTION_OVERLOADS(save_entity_view_ov, ost::mol::alg::StereoChemicalProps (*read_props_a)(String filename, bool check) = &ReadStereoChemicalPropsFile; ost::mol::alg::StereoChemicalProps (*read_props_b)(bool check) = &ReadStereoChemicalPropsFile; -String (*sdf_str_a)(const mol::EntityHandle&)=&EntityToSDFString; -String (*sdf_str_b)(const mol::EntityView&)=&EntityToSDFString; - -void (*save_sdf_handle)(const mol::EntityHandle& entity, const String& filename)=&SaveSDF; -void (*save_sdf_view)(const mol::EntityView& entity, const String& filename)=&SaveSDF; - } void export_pdb_io(); void export_mmcif_io(); void export_omf_io(); void export_map_io(); +void export_sdf_io(); BOOST_PYTHON_MODULE(_ost_io) { class_<IOManager, boost::noncopyable>("IOManager", no_init) @@ -122,14 +115,6 @@ BOOST_PYTHON_MODULE(_ost_io) (arg("seq_list"), arg("filename"), arg("format")="auto")); def("SaveSequence", &SaveSequence, (arg("sequence"), arg("filename"), arg("format")="auto")); - def("LoadSDF", &LoadSDF); - def("SaveSDF", save_sdf_view); - def("SaveSDF", save_sdf_handle); - - def("EntityToSDFStr", sdf_str_a); - def("EntityToSDFStr", sdf_str_b); - - def("SDFStrToEntity", &SDFStringToEntity); def("LoadCRD", &LoadCRD); def("LoadCHARMMTraj_", &LoadCHARMMTraj, (arg("ent"), arg("trj_filename"), @@ -149,6 +134,7 @@ BOOST_PYTHON_MODULE(_ost_io) export_mmcif_io(); export_omf_io(); export_map_io(); + export_sdf_io(); def("SaveCHARMMTraj", &SaveCHARMMTraj, (arg("traj"), arg("pdb_filename"), arg("dcd_filename"), arg("stride")=1, arg("profile")=IOProfile())); diff --git a/modules/io/src/mol/entity_io_sdf_handler.cc b/modules/io/src/mol/entity_io_sdf_handler.cc index 129ce2c86be7a7ec56e1cbee52488a231411067a..cf1ff10c6bd1ec12fb1138e58686c2d839103b59 100644 --- a/modules/io/src/mol/entity_io_sdf_handler.cc +++ b/modules/io/src/mol/entity_io_sdf_handler.cc @@ -21,6 +21,7 @@ */ #include <ost/log.hh> +#include <ost/profile.hh> #include <ost/io/mol/sdf_writer.hh> #include <ost/io/mol/sdf_reader.hh> @@ -37,14 +38,14 @@ bool EntityIOSDFHandler::RequiresProcessor() const void EntityIOSDFHandler::Import(mol::EntityHandle& ent, std::istream& instream) { - SDFReader reader(instream); + SDFReader reader(instream, IOProfileRegistry::Instance().GetDefault()); reader.Import(ent); } void EntityIOSDFHandler::Import(mol::EntityHandle& ent, const boost::filesystem::path& loc) { - SDFReader reader(loc); + SDFReader reader(loc, IOProfileRegistry::Instance().GetDefault()); reader.Import(ent); } diff --git a/modules/io/src/mol/sdf_reader.cc b/modules/io/src/mol/sdf_reader.cc index 70ee382b1671388ee9a22aab0ca43020364a76f6..097e4e4af0a35e3fa9d99291823a10e9c0d13d8e 100644 --- a/modules/io/src/mol/sdf_reader.cc +++ b/modules/io/src/mol/sdf_reader.cc @@ -37,20 +37,20 @@ namespace ost { namespace io { using boost::format; -SDFReader::SDFReader(const String& filename) - : infile_(filename), instream_(infile_) +SDFReader::SDFReader(const String& filename, const IOProfile& profile) + : infile_(filename), instream_(infile_), profile_(profile) { this->ClearState(boost::filesystem::path(filename)); } -SDFReader::SDFReader(const boost::filesystem::path& loc) - : infile_(loc), instream_(infile_) +SDFReader::SDFReader(const boost::filesystem::path& loc, const IOProfile& profile) + : infile_(loc), instream_(infile_), profile_(profile) { this->ClearState(loc); } -SDFReader::SDFReader(std::istream& instream) - : infile_(), instream_(instream) +SDFReader::SDFReader(std::istream& instream, const IOProfile& profile) + : infile_(), instream_(instream), profile_(profile) { this->ClearState(boost::filesystem::path("")); } @@ -355,15 +355,40 @@ void SDFReader::AddBond(const bond_data& bond_tuple, int line_num, mol::EntityHa try { type=boost::lexical_cast<int>(boost::trim_copy(s_type)); - if (type<1 || type>8) { - String msg="Bad bond line %d: Bond type number" - " '%s' not within accepted range (1-8)."; - throw IOException(str(format(msg) % line_num % s_type)); + // From SDF spec: + // bond type 1 = Single, 2 = Double, [Q] Values 4 through 8 are + // 3 = Triple, 4 = Aromatic, for SSS queries oniy. + // 5 = Single or Double, + // 6 = Single or Aromatic, + // 7 = Double or Aromatic, + // 8 = Any + if (type < 1 || type > 8) { + std::stringstream ss; + ss << "Bad bond line " << line_num << ": Bond type number " + << std::to_string(type) << " not within accepted range (1-8)."; + if (profile_.fault_tolerant) { + LOG_ERROR(ss.str()); + } else { + throw IOException(ss.str()); + } + } else if (type > 3) { + std::stringstream ss; + ss << "Bad bond line " << line_num << ": Bond type number " + << std::to_string(type) << ": values 4-8 are reserved for queries, " + << "should not appear in an SDF file."; + LOG_WARNING(ss.str()); } } catch(boost::bad_lexical_cast&) { - String msg="Bad bond line %d: Can't convert bond type number" - " '%s' to integral constant."; - throw IOException(str(format(msg) % line_num % s_type)); + std::stringstream ss; + ss << "Bad bond line " << line_num << ": Can't convert bond type number '" + << s_type << "' to integral constant."; + if (profile_.fault_tolerant) { + ss << " Assuming single bond in fault tolerant mode."; + LOG_ERROR(ss.str()); + type = 1; + } else { + throw IOException(ss.str()); + } } mol::AtomHandle first,second; diff --git a/modules/io/src/mol/sdf_reader.hh b/modules/io/src/mol/sdf_reader.hh index 59e733a937a3ed0dcb7171deec9ff9a32dd808ae..d524a8bf1561323dcbb2b91c10c70097673b352b 100644 --- a/modules/io/src/mol/sdf_reader.hh +++ b/modules/io/src/mol/sdf_reader.hh @@ -28,6 +28,7 @@ #include <ost/mol/chain_handle.hh> #include <ost/mol/residue_handle.hh> #include <ost/io/module_config.hh> +#include <ost/io/mol/io_profile.hh> namespace ost { namespace io { @@ -36,11 +37,9 @@ namespace ost { namespace io { class DLLEXPORT_OST_IO SDFReader { public: - SDFReader(const String& filename); - SDFReader(const boost::filesystem::path& loc); - SDFReader(std::istream& instream); - - bool HasNext(); + SDFReader(const String& filename, const IOProfile& profile); + SDFReader(const boost::filesystem::path& loc, const IOProfile& profile); + SDFReader(std::istream& instream, const IOProfile& profile); void Import(mol::EntityHandle& ent); @@ -97,6 +96,7 @@ private: boost::filesystem::ifstream infile_; std::istream& instream_; boost::iostreams::filtering_stream<boost::iostreams::input> in_; + IOProfile profile_; String version_; bool v3000_atom_block_; bool v3000_bond_block_; diff --git a/modules/io/src/mol/sdf_str.cc b/modules/io/src/mol/sdf_str.cc index a2977c432bf3ec90647de1b05dc8eb5a2afb3f74..0441b672f0edbc3604888cec50abc35f75e0e54b 100644 --- a/modules/io/src/mol/sdf_str.cc +++ b/modules/io/src/mol/sdf_str.cc @@ -37,9 +37,9 @@ String EntityToSDFString(const mol::EntityView& ent) { return stream.str(); } -mol::EntityHandle SDFStringToEntity(const String& sdf) { +mol::EntityHandle SDFStringToEntity(const String& sdf, const IOProfile& profile) { std::stringstream stream(sdf); - SDFReader reader(stream); + SDFReader reader(stream, profile); mol::EntityHandle ent = mol::CreateEntity(); reader.Import(ent); return ent; diff --git a/modules/io/src/mol/sdf_str.hh b/modules/io/src/mol/sdf_str.hh index 87987679ed7ad28a6e3023f536ad6c6fb716f7f7..8cc6974593a202be792cc8fcf788fae6f9b780df 100644 --- a/modules/io/src/mol/sdf_str.hh +++ b/modules/io/src/mol/sdf_str.hh @@ -22,6 +22,7 @@ #include <ost/io/module_config.hh> #include <ost/mol/entity_view.hh> #include <ost/mol/entity_handle.hh> +#include <ost/io/mol/io_profile.hh> namespace ost { namespace io { @@ -33,7 +34,7 @@ String DLLEXPORT_OST_IO EntityToSDFString(const mol::EntityView& ent); mol::EntityHandle DLLEXPORT_OST_IO -SDFStringToEntity(const String& pdb); +SDFStringToEntity(const String& pdb, const IOProfile& profile); }} diff --git a/modules/io/tests/test_io_sdf.py b/modules/io/tests/test_io_sdf.py index 7277a399d5a58ca696f50fb19e0775345999313a..40cee207926556fea2916d0a420fcf7f421dee24 100644 --- a/modules/io/tests/test_io_sdf.py +++ b/modules/io/tests/test_io_sdf.py @@ -47,7 +47,41 @@ class TestSDF(unittest.TestCase): # Charge from atom line is ignored o_at = ent.FindAtom("00001_Simple Ligand", 1, "3") self.assertEqual(o_at.charge, 0) - + + def test_fault_tolerant(self): + """This file has a "dative" bond (type = 9). + This is a non-standard extension from RDKit which should go through only + in fault tolerant mode""" + + with self.assertRaises(Exception): + ent = io.LoadSDF('testfiles/sdf/dative_bond.sdf') + + # Directly with fault_tolerant + PushVerbosityLevel(-1) # Expect message at Error level + ent = io.LoadSDF('testfiles/sdf/dative_bond.sdf', fault_tolerant=True) + PopVerbosityLevel() + self.assertEqual(ent.FindAtom("00001_Simple Ligand", 1, "5").bonds[0].bond_order, 9) + + # Sloppy profile + PushVerbosityLevel(-1) # Expect message at Error level + ent = io.LoadSDF('testfiles/sdf/dative_bond.sdf', profile="SLOPPY") + PopVerbosityLevel() + self.assertEqual(ent.FindAtom("00001_Simple Ligand", 1, "5").bonds[0].bond_order, 9) + + # Sloppy profile set as default + old_profile = io.profiles['DEFAULT'].Copy() + io.profiles['DEFAULT'] = "SLOPPY" + PushVerbosityLevel(-1) # Expect message at Error level + ent = io.LoadSDF('testfiles/sdf/dative_bond.sdf') + PopVerbosityLevel() + self.assertEqual(ent.FindAtom("00001_Simple Ligand", 1, "5").bonds[0].bond_order, 9) + + # Test that a restored default profile has fault_tolerant again + io.profiles['DEFAULT'] = old_profile + with self.assertRaises(Exception): + ent = io.LoadSDF('testfiles/sdf/dative_bond.sdf') + + if __name__== '__main__': from ost import testutils testutils.RunTests() diff --git a/modules/io/tests/testfiles/sdf/dative_bond.sdf b/modules/io/tests/testfiles/sdf/dative_bond.sdf new file mode 100644 index 0000000000000000000000000000000000000000..30ac15cde8ddd239f13f7c47b33f584df0533591 --- /dev/null +++ b/modules/io/tests/testfiles/sdf/dative_bond.sdf @@ -0,0 +1,18 @@ +Simple Ligand + + Teststructure + 6 6 0 0 1 0 999 V2000 + 0.0000 0.0000 0.0000 N 0 3 0 0 0 0 + 1.0000 0.0000 0.0000 C 0 0 0 0 0 0 + 0.0000 1.0000 0.0000 O 0 0 0 0 0 0 + 1.0000 1.0000 0.0000 S 0 0 0 0 0 0 + 2.0000 2.0000 0.0000 C 0 0 0 0 0 0 + -1.0000 -1.0000 0.0000 Cl 0 0 0 0 0 0 + 1 2 2 0 0 0 + 1 3 1 0 0 0 + 1 6 1 0 0 0 + 2 4 1 0 0 0 + 3 4 1 0 0 0 + 4 5 9 0 0 0 +M END +$$$$