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)