From 5f039af0338efb368dc69c1f55384fb60fbbb09c Mon Sep 17 00:00:00 2001 From: nwoodweb <60708568+nwoodweb@users.noreply.github.com> Date: Tue, 31 May 2022 08:56:31 -0400 Subject: [PATCH 1/4] added basic *.mol functionality --- iodata/formats/mol.py | 134 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 iodata/formats/mol.py diff --git a/iodata/formats/mol.py b/iodata/formats/mol.py new file mode 100644 index 000000000..dafdd15ef --- /dev/null +++ b/iodata/formats/mol.py @@ -0,0 +1,134 @@ +# IODATA is an input and output module for quantum chemistry. +# Copyright (C) 2011-2019 The IODATA Development Team +# +# This file is part of IODATA. +# +# IODATA is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 +# of the License, or (at your option) any later version. +# +# IODATA 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see +# -- + +from typing import TextIO, Iterator, Tuple + +import numpy as np + +import iodata + +from ..docstrings import (document_load_one, document_load_many, document_dump_one, + document_dump_many) +from ..iodata import IOData +from ..periodic import sym2num, num2sym, bond2num, num2bond +from ..utils import angstrom, LineIterator + + +__all__ = [] + +PATTERNS = ['*.mol'] + +''' +DEFAULT_ATOM_COLUMNS = [ + ("atnums", None, (), int, + (lambda word: int(word) if word.isdigit() else sym2num[word.title()]), + (lambda atnum: "{:2s}".format(num2sym[atnum]))), + ("atcoords", None, (3,), float, + (lambda word: float(word) * angstrom), + (lambda value: "{:15.10f}".format(value / angstrom))) +] +''' +@document_load_one("MOL", ['atcoords', 'atnums', 'bonds', 'title']) +def load_one(lit: LineIterator) -> dict: + """Do not edit this docstring. It will be overwritten.""" + + title = next(lit).strip() # mol title row 1 + next(lit) # mol timestamp row 2 + next(lit) # mol blank line row 3 + + words = next(lit).split() + natom = int(words[0]) # number atoms col0 row 4 + nbond = int(words[1]) # number bonds from col1 row4 + + atcoords = np.empty((natom, 3), float) # xyz coords with shape 3 + atnums = np.empty(natom, int) # each atom as iterated + bonds = np.empty((nbond, 3), int) # each bond as iterated + + for iatom in range(natom): # iterate entire atom block + words = next(lit).split() # split each atom line + atcoords[iatom, 0] = float(words[0]) * angstrom # coord angstrom + atcoords[iatom, 1] = float(words[1]) * angstrom # coord angstrom + atcoords[iatom, 2] = float(words[2]) * angstrom # coord angstrom + atnums[iatom] = sym2num.get(words[3].title()) # atomic symbol + + for ibond in range(nbond): # iterate entire bond block + words = next(lit).split() # each element in bond line + bonds[ibond, 0] = int(words[0]) -1 # first atom for bond + bonds[ibond, 1] = int(words[1]) -1 # second atom for bond + bonds[ibond, 2] = int(words[2]) # bond type (single,double,triple) +# bonds[ibond, 3] = int(words[3]) # bond sterochemistry??? + + while True: # iterate each commentary until EOF + try: + words = next(lit) # skip each commentary line + except StopIteration: # what if file has wrong termination? + lit.error("WARNING: Molecule specification did not end properly with M END !") + if words == 'M END\n': # what if file has right termination? + break # terminate while loop + #end while loop + + return { + 'title': title, + 'atcoords': atcoords, + 'atnums': atnums, + 'bonds': bonds, + } +#end load_one +@document_load_many("MOL", ['atcoords', 'atnums', 'bonds', 'title']) +def load_many(lit: LineIterator) -> Iterator[dict]: + """Do not edit this docstring. It will be overwritten.""" + + while True: + try: + yield load_one(lit) + except StopIteration: + return + #END while loop +#end load_many +@document_dump_one("MOL", ['atcoords', 'atnums', 'bonds', 'title']) +def dump_one(f: TextIO, data: IOData): + """Do not edit this docstring. It will be overwritten.""" + print(data.title or 'Created with IOData', file = f) # row 1, title + print('', file = f) # row 2, empty + print('', file = f) # row 3, empty + + if data.bonds is None: # What if no bond block data? + nbond = 0 + else: # What if bond block data? + nbond = len(data.bonds) # num bonds is length of bonds array + # populate line 4 with atom number and bond number from file + print("{:3d}{:3d} 0 0 0 0 0 0 0999 V2000".format(data.natom, nbond), file=f) + + for iatom in range(data.natom): # populate entire atom block + n = num2sym[data.atnums[iatom]] + x, y, z = data.atcoords[iatom] / angstrom #convert to arb units + print(f'{x:10.4f}{y:10.4f}{z:10.4f} {n:<3s} 0 0 0 0 0 0 0 0 0 0 0 0', file=f) + + if data.bonds is not None: # populate bond block + for beginatom, endatom, bondtype, stereoiso in data.bonds: + print('{:3d}{:3d}{:3d} 0 0 0 0'.format( + iatom, jatom, bondtype, stereoiso), file=f) + print("M END", file = f) +# end dump_one +@document_dump_many("MOL", ['atcoords', 'atnums', 'bonds', 'title']) +def dump_many(f: TextIO, data: IOData): + """Do not edit this docstring. It will be overwritten.""" + for data in datas: + dump_one(f, data) + From 7339d07e22370b2ff3b3124f009bfad220143f02 Mon Sep 17 00:00:00 2001 From: nwoodweb <60708568+nwoodweb@users.noreply.github.com> Date: Tue, 31 May 2022 08:58:26 -0400 Subject: [PATCH 2/4] added mol functionality --- iodata/formats/mol.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/iodata/formats/mol.py b/iodata/formats/mol.py index dafdd15ef..1734aec17 100644 --- a/iodata/formats/mol.py +++ b/iodata/formats/mol.py @@ -69,10 +69,10 @@ def load_one(lit: LineIterator) -> dict: for ibond in range(nbond): # iterate entire bond block words = next(lit).split() # each element in bond line - bonds[ibond, 0] = int(words[0]) -1 # first atom for bond - bonds[ibond, 1] = int(words[1]) -1 # second atom for bond + bonds[ibond, 0] = int(words[0]) # first atom for bond + bonds[ibond, 1] = int(words[1]) # second atom for bond bonds[ibond, 2] = int(words[2]) # bond type (single,double,triple) -# bonds[ibond, 3] = int(words[3]) # bond sterochemistry??? +# bonds[ibond, 3] = int(words[3]) # bond sterochemistry??? while True: # iterate each commentary until EOF try: From 99b9ee1f1858535716b29bd45ea74920167d15cc Mon Sep 17 00:00:00 2001 From: Nathan Wood Date: Fri, 3 Jun 2022 11:26:44 -0400 Subject: [PATCH 3/4] ./iodata/formats/mol.py is now Roberto Compliant --- iodata/formats/mol.py | 100 ++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 57 deletions(-) diff --git a/iodata/formats/mol.py b/iodata/formats/mol.py index 1734aec17..79f66b1a0 100644 --- a/iodata/formats/mol.py +++ b/iodata/formats/mol.py @@ -16,64 +16,57 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- +"""MOL File Format. -from typing import TextIO, Iterator, Tuple +This script handles either individual molecules or a molecular database +originating from V2000 compliant *.mol molecules. -import numpy as np +More information on *.mol files can be found here +https://en.wikipedia.org/wiki/Chemical_table_file +""" + +from typing import TextIO, Iterator -import iodata +import numpy as np -from ..docstrings import (document_load_one, document_load_many, document_dump_one, - document_dump_many) +from ..docstrings import (document_load_one, document_load_many) +from ..docstrings import (document_dump_one, document_dump_many) from ..iodata import IOData -from ..periodic import sym2num, num2sym, bond2num, num2bond +from ..periodic import sym2num, num2sym from ..utils import angstrom, LineIterator __all__ = [] PATTERNS = ['*.mol'] - -''' -DEFAULT_ATOM_COLUMNS = [ - ("atnums", None, (), int, - (lambda word: int(word) if word.isdigit() else sym2num[word.title()]), - (lambda atnum: "{:2s}".format(num2sym[atnum]))), - ("atcoords", None, (3,), float, - (lambda word: float(word) * angstrom), - (lambda value: "{:15.10f}".format(value / angstrom))) -] -''' + + @document_load_one("MOL", ['atcoords', 'atnums', 'bonds', 'title']) -def load_one(lit: LineIterator) -> dict: +def load_one(lit: LineIterator) -> dict: """Do not edit this docstring. It will be overwritten.""" - title = next(lit).strip() # mol title row 1 next(lit) # mol timestamp row 2 - next(lit) # mol blank line row 3 - + next(lit) # mol blank line row 3 words = next(lit).split() + if words[-1].upper() != "V2000": + lit.error("Only V2000 SDF files are supported.") natom = int(words[0]) # number atoms col0 row 4 nbond = int(words[1]) # number bonds from col1 row4 - - atcoords = np.empty((natom, 3), float) # xyz coords with shape 3 + atcoords = np.empty((natom, 3), float) # xyz coords with shape 3 atnums = np.empty(natom, int) # each atom as iterated bonds = np.empty((nbond, 3), int) # each bond as iterated - for iatom in range(natom): # iterate entire atom block - words = next(lit).split() # split each atom line + words = next(lit).split() # split each atom line atcoords[iatom, 0] = float(words[0]) * angstrom # coord angstrom atcoords[iatom, 1] = float(words[1]) * angstrom # coord angstrom - atcoords[iatom, 2] = float(words[2]) * angstrom # coord angstrom + atcoords[iatom, 2] = float(words[2]) * angstrom # coord angstrom atnums[iatom] = sym2num.get(words[3].title()) # atomic symbol - for ibond in range(nbond): # iterate entire bond block words = next(lit).split() # each element in bond line bonds[ibond, 0] = int(words[0]) # first atom for bond - bonds[ibond, 1] = int(words[1]) # second atom for bond + bonds[ibond, 1] = int(words[1]) # second atom for bond bonds[ibond, 2] = int(words[2]) # bond type (single,double,triple) # bonds[ibond, 3] = int(words[3]) # bond sterochemistry??? - while True: # iterate each commentary until EOF try: words = next(lit) # skip each commentary line @@ -81,54 +74,47 @@ def load_one(lit: LineIterator) -> dict: lit.error("WARNING: Molecule specification did not end properly with M END !") if words == 'M END\n': # what if file has right termination? break # terminate while loop - #end while loop - - return { + return{ 'title': title, 'atcoords': atcoords, 'atnums': atnums, - 'bonds': bonds, - } -#end load_one + 'bonds': bonds} + + @document_load_many("MOL", ['atcoords', 'atnums', 'bonds', 'title']) def load_many(lit: LineIterator) -> Iterator[dict]: """Do not edit this docstring. It will be overwritten.""" - while True: try: yield load_one(lit) except StopIteration: - return - #END while loop -#end load_many -@document_dump_one("MOL", ['atcoords', 'atnums', 'bonds', 'title']) + return + + +@document_dump_one("MOL", ['atcoords', 'atnums', 'bonds', 'title']) def dump_one(f: TextIO, data: IOData): """Do not edit this docstring. It will be overwritten.""" - print(data.title or 'Created with IOData', file = f) # row 1, title - print('', file = f) # row 2, empty - print('', file = f) # row 3, empty - + print(data.title or 'Created with IOData', file=f) # row 1, title + print('', file=f) # row 2, empty + print('', file=f) # row 3, empty if data.bonds is None: # What if no bond block data? nbond = 0 else: # What if bond block data? nbond = len(data.bonds) # num bonds is length of bonds array # populate line 4 with atom number and bond number from file print("{:3d}{:3d} 0 0 0 0 0 0 0999 V2000".format(data.natom, nbond), file=f) - - for iatom in range(data.natom): # populate entire atom block - n = num2sym[data.atnums[iatom]] - x, y, z = data.atcoords[iatom] / angstrom #convert to arb units + for beginatom in range(data.natom): # populate entire atom block + n = num2sym[data.atnums[beginatom]] + x, y, z = data.atcoords[beginatom] / angstrom # convert to arb units print(f'{x:10.4f}{y:10.4f}{z:10.4f} {n:<3s} 0 0 0 0 0 0 0 0 0 0 0 0', file=f) - if data.bonds is not None: # populate bond block - for beginatom, endatom, bondtype, stereoiso in data.bonds: - print('{:3d}{:3d}{:3d} 0 0 0 0'.format( - iatom, jatom, bondtype, stereoiso), file=f) - print("M END", file = f) -# end dump_one -@document_dump_many("MOL", ['atcoords', 'atnums', 'bonds', 'title']) -def dump_many(f: TextIO, data: IOData): + for beginatom, endatom, bondtype in data.bonds: + print('{:3d}{:3d}{:3d} 0 0 0 0'.format(beginatom, endatom, bondtype), file=f) + print("M END", file=f) + + +@document_dump_many("MOL", ['atcoords', 'atnums', 'bonds', 'title']) +def dump_many(f: TextIO, datas: IOData): """Do not edit this docstring. It will be overwritten.""" for data in datas: dump_one(f, data) - From be0d4e3a6bf6695ced121233daeef8b63df78f01 Mon Sep 17 00:00:00 2001 From: Nathan Wood Date: Sat, 4 Jun 2022 16:33:34 -0400 Subject: [PATCH 4/4] add unit tests for mol implementation, roberto compliant --- iodata/formats/mol.py | 26 ++--- iodata/test/data/benzoicacid.mol | 35 +++++++ iodata/test/data/database.mol | 20 ++++ iodata/test/data/water.mol | 10 ++ iodata/test/data/waterv3000.mol | 10 ++ iodata/test/test_mol.py | 160 +++++++++++++++++++++++++++++++ 6 files changed, 248 insertions(+), 13 deletions(-) create mode 100644 iodata/test/data/benzoicacid.mol create mode 100644 iodata/test/data/database.mol create mode 100644 iodata/test/data/water.mol create mode 100644 iodata/test/data/waterv3000.mol create mode 100644 iodata/test/test_mol.py diff --git a/iodata/formats/mol.py b/iodata/formats/mol.py index 79f66b1a0..76a56e5a3 100644 --- a/iodata/formats/mol.py +++ b/iodata/formats/mol.py @@ -61,19 +61,18 @@ def load_one(lit: LineIterator) -> dict: atcoords[iatom, 1] = float(words[1]) * angstrom # coord angstrom atcoords[iatom, 2] = float(words[2]) * angstrom # coord angstrom atnums[iatom] = sym2num.get(words[3].title()) # atomic symbol - for ibond in range(nbond): # iterate entire bond block - words = next(lit).split() # each element in bond line - bonds[ibond, 0] = int(words[0]) # first atom for bond - bonds[ibond, 1] = int(words[1]) # second atom for bond - bonds[ibond, 2] = int(words[2]) # bond type (single,double,triple) -# bonds[ibond, 3] = int(words[3]) # bond sterochemistry??? - while True: # iterate each commentary until EOF + for ibond in range(nbond): # iterate entire bond block + words = next(lit).split() # each element in bond line + bonds[ibond, 0] = int(words[0]) - 1 # first atom for bond + bonds[ibond, 1] = int(words[1]) - 1 # second atom for bond + bonds[ibond, 2] = int(words[2]) # bond type (single,double,triple) + while True: # iterate each commentary until EOF try: - words = next(lit) # skip each commentary line - except StopIteration: # what if file has wrong termination? + words = next(lit) # skip each commentary line + except StopIteration: # what if file has wrong termination? lit.error("WARNING: Molecule specification did not end properly with M END !") - if words == 'M END\n': # what if file has right termination? - break # terminate while loop + if words == "M END\n": # what if file has right termination? + break # terminate while loop return{ 'title': title, 'atcoords': atcoords, @@ -97,7 +96,7 @@ def dump_one(f: TextIO, data: IOData): print(data.title or 'Created with IOData', file=f) # row 1, title print('', file=f) # row 2, empty print('', file=f) # row 3, empty - if data.bonds is None: # What if no bond block data? + if data.bonds is None: # What if no bond block data? nbond = 0 else: # What if bond block data? nbond = len(data.bonds) # num bonds is length of bonds array @@ -109,7 +108,8 @@ def dump_one(f: TextIO, data: IOData): print(f'{x:10.4f}{y:10.4f}{z:10.4f} {n:<3s} 0 0 0 0 0 0 0 0 0 0 0 0', file=f) if data.bonds is not None: # populate bond block for beginatom, endatom, bondtype in data.bonds: - print('{:3d}{:3d}{:3d} 0 0 0 0'.format(beginatom, endatom, bondtype), file=f) + print('{:3d}{:3d}{:3d} 0 0 0 0'.format(beginatom + 1, endatom + 1, + bondtype), file=f) print("M END", file=f) diff --git a/iodata/test/data/benzoicacid.mol b/iodata/test/data/benzoicacid.mol new file mode 100644 index 000000000..35689506f --- /dev/null +++ b/iodata/test/data/benzoicacid.mol @@ -0,0 +1,35 @@ +C7H6O2 +APtclcactv06032219373D 0 0.00000 0.00000 + + 15 15 0 0 0 0 0 0 0 0999 V2000 + 1.6471 0.0768 -0.0012 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.1706 0.0297 -0.0002 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.5689 1.2141 0.0004 C 0 0 0 0 0 0 0 0 0 0 0 0 + -1.9473 1.1628 0.0009 C 0 0 0 0 0 0 0 0 0 0 0 0 + -2.5974 -0.0586 0.0013 C 0 0 0 0 0 0 0 0 0 0 0 0 + -1.8708 -1.2362 0.0011 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.4918 -1.1994 -0.0057 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.2206 1.1476 -0.0015 O 0 0 0 0 0 0 0 0 0 0 0 0 + 2.3575 -1.0677 0.0038 O 0 0 0 0 0 0 0 0 0 0 0 0 + -0.0627 2.1682 0.0009 H 0 0 0 0 0 0 0 0 0 0 0 0 + -2.5205 2.0782 0.0014 H 0 0 0 0 0 0 0 0 0 0 0 0 + -3.6768 -0.0931 0.0020 H 0 0 0 0 0 0 0 0 0 0 0 0 + -2.3844 -2.1862 0.0021 H 0 0 0 0 0 0 0 0 0 0 0 0 + 0.0741 -2.1193 -0.0061 H 0 0 0 0 0 0 0 0 0 0 0 0 + 3.3211 -0.9864 0.0029 H 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 + 4 5 2 0 0 0 0 + 5 6 1 0 0 0 0 + 6 7 2 0 0 0 0 + 2 7 1 0 0 0 0 + 1 8 2 0 0 0 0 + 1 9 1 0 0 0 0 + 3 10 1 0 0 0 0 + 4 11 1 0 0 0 0 + 5 12 1 0 0 0 0 + 6 13 1 0 0 0 0 + 7 14 1 0 0 0 0 + 9 15 1 0 0 0 0 +M END diff --git a/iodata/test/data/database.mol b/iodata/test/data/database.mol new file mode 100644 index 000000000..4fed70487 --- /dev/null +++ b/iodata/test/data/database.mol @@ -0,0 +1,20 @@ +H2O +APtclcactv06032219383D 0 0.00000 0.00000 + + 3 2 0 0 0 0 0 0 0 0999 V2000 + -0.0000 -0.0589 -0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0 + -0.8110 0.4677 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0 + 0.8110 0.4677 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 0 0 0 + 1 3 1 0 0 0 0 +M END +H2O +APtclcactv06032219383D 0 0.00000 0.00000 + + 3 2 0 0 0 0 0 0 0 0999 V2000 + -0.0000 -0.0589 -0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0 + -0.8110 0.4677 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0 + 0.8110 0.4677 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 0 0 0 + 1 3 1 0 0 0 0 +M END diff --git a/iodata/test/data/water.mol b/iodata/test/data/water.mol new file mode 100644 index 000000000..2cb441342 --- /dev/null +++ b/iodata/test/data/water.mol @@ -0,0 +1,10 @@ +H2O +APtclcactv06032219383D 0 0.00000 0.00000 + + 3 2 0 0 0 0 0 0 0 0999 V2000 + -0.0000 -0.0589 -0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0 + -0.8110 0.4677 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0 + 0.8110 0.4677 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 0 0 0 + 1 3 1 0 0 0 0 +M END diff --git a/iodata/test/data/waterv3000.mol b/iodata/test/data/waterv3000.mol new file mode 100644 index 000000000..e043c5aa1 --- /dev/null +++ b/iodata/test/data/waterv3000.mol @@ -0,0 +1,10 @@ +H2O +APtclcactv06032219383D 0 0.00000 0.00000 + + 3 2 0 0 0 0 0 0 0 0999 V3000 + -0.0000 -0.0589 -0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0 + -0.8110 0.4677 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0 + 0.8110 0.4677 0.0000 H 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 0 0 0 + 1 3 1 0 0 0 0 +M END diff --git a/iodata/test/test_mol.py b/iodata/test/test_mol.py new file mode 100644 index 000000000..4d7976eee --- /dev/null +++ b/iodata/test/test_mol.py @@ -0,0 +1,160 @@ +# IODATA is an input and output module for quantum chemistry. +# Copyright (C) 2011-2019 The IODATA Development Team +# +# This file is part of IODATA. +# +# IODATA is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 3 +# of the License, or (at your option) any later version. +# +# IODATA 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, see +# -- +"""Test iodata.formats.mol module.""" + +import os + +import pytest +from numpy.testing import assert_equal, assert_allclose + +from .common import truncated_file +from ..api import load_one, load_many, dump_one, dump_many +from ..utils import angstrom, FileFormatError + +try: + from importlib_resources import path +except ImportError: + from importlib.resources import path + + +def test_mol_load_one_water(): + # test mol one structure + with path('iodata.test.data', 'water.mol') as fn_mol: + mol = load_one(str(fn_mol)) + assert mol.title == 'H2O' + assert mol.natom == 3 + assert len(mol.bonds) == 2 + assert_equal(mol.atnums, [8, 1, 1]) + # check coordinates + atcoords_ang = mol.atcoords / angstrom + assert_allclose(atcoords_ang[0], [-0.0000, -0.0589, -0.0000]) + assert_allclose(atcoords_ang[1], [-0.8110, 0.4677, 0.0000]) + assert_allclose(atcoords_ang[2], [0.8110, 0.4677, 0.0000]) + assert_equal(mol.bonds[0], [0, 1, 1]) + assert_equal(mol.bonds[1], [0, 2, 1]) + + +def test_mol_load_one_benzoicacid(): + # test mol one structure + with path('iodata.test.data', 'benzoicacid.mol') as fn_mol: + mol = load_one(str(fn_mol)) + assert mol.title == 'C7H6O2' + assert mol.natom == 15 + assert len(mol.bonds) == 15 + assert_equal(mol.atnums, [6, 6, 6, 6, 6, 6, 6, 8, 8, 1, 1, 1, 1, 1, 1]) + atcoords_ang = mol.atcoords / angstrom + # test atoms + assert_allclose(atcoords_ang[0], [1.6471, 0.0768, -0.0012]) + assert_allclose(atcoords_ang[1], [0.1706, 0.0297, -0.0002]) + assert_allclose(atcoords_ang[2], [-0.5689, 1.2141, 0.0004]) + assert_allclose(atcoords_ang[3], [-1.9473, 1.1628, 0.0009]) + assert_allclose(atcoords_ang[4], [-2.5974, -0.0586, 0.0013]) + assert_allclose(atcoords_ang[5], [-1.8708, -1.2362, 0.0011]) + assert_allclose(atcoords_ang[6], [-0.4918, -1.1994, -0.0057]) + assert_allclose(atcoords_ang[7], [2.2206, 1.1476, -0.0015]) + assert_allclose(atcoords_ang[8], [2.3575, -1.0677, 0.0038]) + assert_allclose(atcoords_ang[9], [-0.0627, 2.1682, 0.0009]) + assert_allclose(atcoords_ang[10], [-2.5205, 2.0782, 0.0014]) + assert_allclose(atcoords_ang[11], [-3.6768, -0.0931, 0.0020]) + assert_allclose(atcoords_ang[12], [-2.3844, -2.1862, 0.0021]) + assert_allclose(atcoords_ang[13], [0.0741, -2.1193, -0.0061]) + assert_allclose(atcoords_ang[14], [3.3211, -0.9864, 0.0029]) + # test bonds + assert_equal(mol.bonds[0], [0, 1, 1]) + assert_equal(mol.bonds[1], [1, 2, 2]) + assert_equal(mol.bonds[2], [2, 3, 1]) + assert_equal(mol.bonds[3], [3, 4, 2]) + assert_equal(mol.bonds[4], [4, 5, 1]) + assert_equal(mol.bonds[5], [5, 6, 2]) + assert_equal(mol.bonds[6], [1, 6, 1]) + assert_equal(mol.bonds[7], [0, 7, 2]) + assert_equal(mol.bonds[8], [0, 8, 1]) + assert_equal(mol.bonds[9], [2, 9, 1]) + assert_equal(mol.bonds[10], [3, 10, 1]) + assert_equal(mol.bonds[11], [4, 11, 1]) + assert_equal(mol.bonds[12], [5, 12, 1]) + assert_equal(mol.bonds[13], [6, 13, 1]) + assert_equal(mol.bonds[14], [8, 14, 1]) + + +def test_mol_formaterror(tmpdir): + # test mol file if it has an ending other than 'M END' + with path('iodata.test.data', 'benzoicacid.mol') as fn_test: + with truncated_file(fn_test, 15, 0, tmpdir) as fn: + with pytest.raises(IOError): + load_one(str(fn)) + + +def check_load_dump_consistency(tmpdir, fn): + """Check if dumping and loading an MOL file results in the same data.""" + mol0 = load_one(str(fn)) + fn_tmp = os.path.join(tmpdir, 'test.mol') + dump_one(mol0, fn_tmp) + mol1 = load_one(fn_tmp) + assert mol0.title == mol1.title + assert_equal(mol0.atnums, mol1.atnums) + assert_allclose(mol0.atcoords, mol1.atcoords, atol=1.e-5) + assert_equal(mol0.bonds, mol1.bonds) + + +def test_dump_consistency(tmpdir): + with path('iodata.test.data', 'water.mol') as fn_mol: + check_load_dump_consistency(tmpdir, fn_mol) + with path('iodata.test.data', 'benzoicacid.mol') as fn_mol: + check_load_dump_consistency(tmpdir, fn_mol) + with path('iodata.test.data', 'benzene.mol2') as fn_mol: + check_load_dump_consistency(tmpdir, fn_mol) + + +def test_load_many(): + with path('iodata.test.data', 'database.mol') as fn_mol: + mols = list(load_many(str(fn_mol))) + assert len(mols) == 2 + assert mols[1].title == 'H2O' + assert mols[0].title == 'H2O' + assert_equal(mols[0].bonds[0], [0, 1, 1]) + assert_equal(mols[1].bonds[0], [0, 1, 1]) + assert_equal(mols[0].bonds[1], [0, 2, 1]) + assert_equal(mols[1].bonds[1], [0, 2, 1]) + assert_equal(mols[0].natom, mols[1].natom) + assert_equal(mols[1].natom, 3) + assert_allclose(mols[0].atcoords[0] / angstrom, [-0.0000, -0.0589, -0.0000]) + assert_allclose(mols[1].atcoords[0] / angstrom, [-0.0000, -0.0589, -0.0000]) + assert_allclose(mols[0].atcoords[1] / angstrom, [-0.8110, 0.4677, 0.0000]) + assert_allclose(mols[1].atcoords[1] / angstrom, [-0.8110, 0.4677, 0.0000]) + + +def test_load_dump_many_consistency(tmpdir): + with path('iodata.test.data', 'water.mol') as fn_mol: + mols0 = list(load_many(str(fn_mol))) + fn_tmp = os.path.join(tmpdir, 'test') + dump_many(mols0, fn_tmp, fmt='mol') + mols1 = list(load_many(fn_tmp, fmt='mol')) + assert len(mols0) == len(mols1) + for mol0, mol1 in zip(mols0, mols1): + assert mol0.title == mol1.title + assert_equal(mol0.atnums, mol1.atnums) + assert_allclose(mol0.atcoords, mol1.atcoords, atol=1.e-5) + assert_equal(mol0.bonds, mol1.bonds) + + +def test_v2000_check(): + with path('iodata.test.data', 'waterv3000.mol') as fn_mol: + with pytest.raises(FileFormatError): + load_one(fn_mol)