Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mwfn load_one #198

Merged
merged 33 commits into from Mar 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7093ba8
Add mwfn test files converted from fchk
leila-pujal Aug 12, 2020
23a24b7
initial commit for mwfn.py contains load_one
BradenDKelly Aug 25, 2020
c5b5223
added test_mwfn.py
BradenDKelly Aug 25, 2020
ca6ed44
cleanup in test_mwfn.py and mwfn.py for formatting
BradenDKelly Aug 25, 2020
85d4bf6
Fix header & pycodestyle E241 & E501
FarnazH Aug 26, 2020
8d06fc0
Fix pylint issues
FarnazH Aug 26, 2020
cab2fde
Fix some more pylint issues. :)
tovrstra Aug 26, 2020
e8855d6
Remove unused code
tovrstra Aug 26, 2020
5434810
cleanup and formatting.
BradenDKelly Aug 27, 2020
02894dc
fix white spaces for travis
BradenDKelly Aug 27, 2020
cf2eb66
Move keys inside _load_helper_opener function
FarnazH Aug 28, 2020
aa27305
Have _load_helper_opener function return a dict
FarnazH Aug 28, 2020
941635e
Have _load_helper_atoms function return a dict
FarnazH Aug 28, 2020
b642269
Have _load_helper_basis/shells funcs return dict
FarnazH Aug 28, 2020
4ea965f
Assign primitives exponents & coeffs to data dict
FarnazH Aug 28, 2020
26718d2
Have _load_helper_mo return dict containing all MO
FarnazH Aug 28, 2020
f7426e0
Have _load_mwfn_low function return dict
FarnazH Aug 28, 2020
8e08464
Add mo_kind key to loaded MWFN data
FarnazH Aug 28, 2020
3149ff7
Move or remove assert checks in mwfn load_one
FarnazH Aug 28, 2020
05494e5
Remove _load_helper_prims function
FarnazH Aug 28, 2020
4e2d6da
Remove _build_obasis function
FarnazH Aug 28, 2020
43b22e0
Turn asserts to raises in _load_helper_shells func
FarnazH Aug 28, 2020
b2b0534
Turn assert to raise in _load_helper_section func
FarnazH Aug 28, 2020
7d3f67f
Remove redundant items from extra dictionary
FarnazH Aug 29, 2020
ab3e94b
Update docstrings & comments
FarnazH Aug 29, 2020
df6df62
Fix pylint literal-comparison
FarnazH Aug 29, 2020
b51453f
formatting and conversion simplifications in mwfn.py
BradenDKelly Aug 31, 2020
a006bfb
docstring and assert_allclose for coeffs in test_mwfn.py
BradenDKelly Aug 31, 2020
fe1f842
remove helper_load_mwfn_low function
BradenDKelly Sep 1, 2020
29e26b4
Merge branch 'master' into MWFN_LoadOne_New
tovrstra Mar 21, 2021
8896d53
Fix initialization of Orbitals and clarify docstring
tovrstra Mar 22, 2021
f876bfe
Remove 'format' wording, very minor
tovrstra Mar 22, 2021
7d081f9
Reduce items stored in extra dictionary of MWFN
FarnazH Mar 29, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
329 changes: 329 additions & 0 deletions iodata/formats/mwfn.py
@@ -0,0 +1,329 @@
# 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 <http://www.gnu.org/licenses/>
# --
"""Multiwfn MWFN file format."""


import numpy as np

from ..basis import HORTON2_CONVENTIONS, MolecularBasis, Shell
from ..docstrings import document_load_one
from ..orbitals import MolecularOrbitals
from ..utils import LineIterator, angstrom


__all__ = []


PATTERNS = ['*.mwfn']


# From the MWFN chemrxiv paper
# https://chemrxiv.org/articles/Mwfn_A_Strict_Concise_and_Extensible_Format
# _for_Electronic_Wavefunction_Storage_and_Exchange/11872524
# For cartesian shells
# S shell: S
# P shell: X, Y, Z
# D shell: XX, YY, ZZ, XY, XZ, YZ
# F shell: XXX, YYY, ZZZ, XYY, XXY, XXZ, XZZ, YZZ, YYZ, XYZ
# G shell: ZZZZ, YZZZ, YYZZ, YYYZ, YYYY, XZZZ, XYZZ, XYYZ, XYYY, XXZZ, XXYZ, XXYY, XXXZ, XXXY, XXXX
# H shell: ZZZZZ, YZZZZ, YYZZZ, YYYZZ, YYYYZ, YYYYY, XZZZZ, XYZZZ, XYYZZ, XYYYZ, XYYYY, XXZZZ,
# XXYZZ, XXYYZ, XXYYY, XXXZZ, XXXYZ, XXXYY, XXXXZ, XXXXY, XXXXX
tovrstra marked this conversation as resolved.
Show resolved Hide resolved
# For pure shells, the order is
# D shell: D 0, D+1, D-1, D+2, D-2
# F shell: F 0, F+1, F-1, F+2, F-2, F+3, F-3
# G shell: G 0, G+1, G-1, G+2, G-2, G+3, G-3, G+4, G-4

CONVENTIONS = {
(4, 'p'): HORTON2_CONVENTIONS[(4, 'p')],
(3, 'p'): HORTON2_CONVENTIONS[(3, 'p')],
(2, 'p'): HORTON2_CONVENTIONS[(2, 'p')],
(0, 'c'): ['1'],
(1, 'c'): ['x', 'y', 'z'],
(2, 'c'): ['xx', 'yy', 'zz', 'xy', 'xz', 'yz'],
(3, 'c'): ['xxx', 'yyy', 'zzz', 'xyy', 'xxy', 'xxz', 'xzz', 'yzz', 'yyz', 'xyz'],
(4, 'c'): ['zzzz', 'yzzz', 'yyzz', 'yyyz', 'yyyy', 'xzzz', 'xyzz', 'xyyz', 'xyyy',
'xxzz', 'xxyz', 'xxyy', 'xxxz', 'xxxy', 'xxxx'],
(5, 'c'): ['zzzzz', 'yzzzz', 'yyzzz', 'yyyzz', 'yyyyz', 'yyyyy', 'xzzzz', 'xyzzz',
'xyyzz', 'xyyyz', 'xyyyy', 'xxzzz', 'xxyzz', 'xxyyz', 'xxyyy', 'xxxzz',
'xxxyz', 'xxxyy', 'xxxxz', 'xxxxy', 'xxxxx'],
}


def _load_helper_opener(lit: LineIterator) -> dict:
"""Read initial variables at the beginning of a MWFN file."""
keys = {"Wfntype": int, "Charge": float, "Naelec": float, "Nbelec": float, "E_tot": float,
"VT_ratio": float, "Ncenter": int}
max_count = len(keys)
count = 0
data = {}
while count < max_count:
line = next(lit)
for name, ftype in keys.items():
if name in line:
data[name] = ftype(line.split('=')[1].strip())
count += 1

# check values parsed
if data["Ncenter"] <= 0:
lit.error(f"Ncenter should be a positive integer! Read Ncenter= {data['Ncenter']}")
# Possible values of Wfntype (wavefunction type):
# 0: Restricted closed - shell single - determinant wavefunction(e.g.RHF, RKS)
# 1: Unrestricted open - shell single - determinant wavefunction(e.g.UHF, UKS)
# 2: Restricted open - shell single - determinant wavefunction(e.g.ROHF, ROKS)
# 3: Restricted multiconfiguration wavefunction(e.g.RMP2, RCCSD)
# 4: Unrestricted multiconfiguration wavefunction(e.g.UMP2, UCCSD)
if data["Wfntype"] in [0, 2, 3]:
# restricted wavefunction
data["mo_kind"] = "restricted"
elif data["Wfntype"] in [1, 4]:
# unrestricted wavefunction
data["mo_kind"] = "unrestricted"
else:
lit.error(f"Wavefunction type cannot be determined. Read Wfntype= {data['Wfntype']}")

return data


def _load_helper_basis(lit: LineIterator) -> dict:
"""Read basis set information typically labelled '# Basis function information'."""
# Nprims must be last or else it gets read in with Nprimshell
keys = ["Nbasis", "Nindbasis", "Nshell", "Nprimshell", "Nprims"]
count = 0
data = {}
next(lit)
while count < len(keys):
line = next(lit)
for name in keys:
if name in line:
data[name] = int(line.split('=')[1].strip())
count += 1
break
return data


def _load_helper_atoms(lit: LineIterator, natom: int) -> dict:
"""Read atoms section typically labelled '# Atom information'."""
data = {
"atnums": np.empty(natom, int),
"atcorenums": np.empty(natom, float),
"atcoords": np.empty((natom, 3), float),
}

# skip lines until "$Centers" section is reached
line = next(lit)
while '$Centers' not in line and line is not None:
line = next(lit)

for atom in range(natom):
words = next(lit).split()
data["atnums"][atom] = words[2]
data["atcorenums"][atom] = words[3]
data["atcoords"][atom, :] = words[4:7]
# coordinates are in angstrom in MWFN, so they are converted to atomic units
data["atcoords"] *= angstrom

# check atomic numbers
if min(data["atnums"]) <= 0:
lit.error(f"Atomic numbers should be positive integers! Read atnums= {data['atnums']}")

return data


def _load_helper_shells(lit: LineIterator, nshell: int) -> dict:
"""Read shell types, centers, and contraction degrees."""
sections = ["$Shell types", "$Shell centers", "$Shell contraction"]
var_name = ["shell_types", "shell_centers", "shell_ncons"]

# read lines until '$Shell types' is reached
line = next(lit)
while sections[0] not in line and line is not None:
line = next(lit)

data = {}
for section, name in zip(sections, var_name):
if not line.startswith(section):
lit.error(f"Expected line to start with {section}, but got line={line}.")
data[name] = _load_helper_section(lit, nshell, ' ', 0, int)
line = next(lit)
lit.back(line)
return data


def _load_helper_section(lit: LineIterator, nprim: int, start: str, skip: int,
dtype: np.dtype) -> np.ndarray:
"""Read single or multiple line(s) section."""
section = []
while len(section) < nprim:
line = next(lit)
if not line.startswith(start):
lit.error(f"Expected line to start with {start}! Got line={line}")
section.extend(line.split()[skip:])
return np.array(section, dtype)


def _load_helper_mo(lit: LineIterator, n_basis: int, n_mo: int) -> dict:
"""Read molecular orbitals section typically labelled '# Orbital information'."""
data = {
"mo_numbers": np.empty(n_mo, int),
"mo_type": np.empty(n_mo, int),
"mo_energies": np.empty(n_mo, float),
"mo_occs": np.empty(n_mo, float),
"mo_sym": np.empty(n_mo, str),
"mo_coeffs": np.empty([n_basis, n_mo], float),
}

for index in range(n_mo):
line = next(lit)
while 'Index' not in line:
line = next(lit)
assert line.startswith('Index')
data["mo_numbers"][index] = line.split()[1]
data["mo_type"][index] = next(lit).split()[1]
data["mo_energies"][index] = next(lit).split()[1]
data["mo_occs"][index] = next(lit).split()[1]
data["mo_sym"][index] = next(lit).split()[1]
# skip "$Coeff line
next(lit)
data["mo_coeffs"][:, index] = _load_helper_section(lit, n_basis, '', 0, float)

return data


def _load_mwfn_low(lit: LineIterator) -> dict:
"""Load data from a MWFN file into arrays.

Parameters
----------
lit
The line iterator to read the data from.
"""
# Note:
# -----
# 1) mwfn is a fortran program which loads *.mwfn by locating the line with the keyword,
# then uses `backspace`, then begins reading. Despite this flexibility, it is stated by
# the authors that the order of section, and indeed, entries in general, must be fixed.
# With this in mind the input utilized some hard-coding since order should be fixed.
# 2) mwfn ignores lines beginning with `#`.

# read title (assuming title is the 1st line, which seems to be the standard)
data = {"title": next(lit).strip()}

# load Wfntype, Charge, Naelec, Nbelec, E_tot, VT_ratio, & Ncenter
data.update(_load_helper_opener(lit))

# load atnums, atcorenums, & atcoords (in atomic units)
data.update(_load_helper_atoms(lit, data["Ncenter"]))

# load Nbasis, Nindbasis, Nprims, Nshell, & Nprimshell
data.update(_load_helper_basis(lit))

# load shell_types, shell_centers, & shell_contraction_degrees
data.update(_load_helper_shells(lit, data["Nshell"]))
# IOData indices start at 0, so the centers are shifted
data["shell_centers"] -= 1

# load primitive exponents & coefficients
if not next(lit).startswith("$Primitive exponents"):
lit.error("Expected '$Primitive exponents' section!")
data["exponents"] = _load_helper_section(lit, data["Nprimshell"], '', 0, float)
if not next(lit).startswith("$Contraction coefficients"):
lit.error("Expected '$Contraction coefficients' section!")
data["coeffs"] = _load_helper_section(lit, data["Nprimshell"], '', 0, float)

# get number of basis & molecular orbitals (MO)
# Note: MWFN includes virtual orbitals, so num_mo equals number independent basis functions
num_basis = data["Nindbasis"]
num_mo = data["Nindbasis"]
if data["mo_kind"] == "unrestricted":
num_mo *= 2
# load MO information
data.update(_load_helper_mo(lit, num_basis, num_mo))

return data


@document_load_one("MWFN", ['atcoords', 'atnums', 'atcorenums', 'energy',
'mo', 'obasis', 'extra', 'title'])
def load_one(lit: LineIterator) -> dict:
"""Do not edit this docstring. It will be overwritten."""
inp = _load_mwfn_low(lit)

# store certain information loaded from MWFN in extra dictionary
extra = {'wfntype': inp['Wfntype'],
'nindbasis': inp['Nindbasis'],
'mo_sym': inp['mo_sym'],
'full_virial_ratio': inp['VT_ratio']}

# Build MolecularBasis instance
# Unlike WFN, MWFN does include orbital expansion coefficients.
shells = []
counter = 0
for center, stype, ncon in zip(inp['shell_centers'], inp['shell_types'], inp['shell_ncons']):
shells.append(Shell(
center,
[abs(stype)],
['p' if stype < 0 else 'c'],
inp['exponents'][counter:counter + ncon],
inp['coeffs'][counter:counter + ncon][:, np.newaxis]
))
counter += ncon
obasis = MolecularBasis(shells, CONVENTIONS, 'L2')
# check number of basis functions
if obasis.nbasis != inp["Nbasis"]:
raise ValueError(f"Number of basis in MolecularBasis is not equal to the 'Nbasis'. "
f"{obasis.nbasis} != {inp['Nbasis']}")

# Determine number of orbitals of each kind.
if inp["mo_kind"] == "restricted":
norba = len(inp["mo_type"])
norbb = norba
else:
# Legend
# 0: restricted doubly occupied
# 1: alpha
# 2: beta
norba = (inp["mo_type"] == 1).sum()
norbb = (inp["mo_type"] == 2).sum()
assert (inp["mo_type"] == 0).sum() == 0
# Build MolecularOrbitals instance
mo = MolecularOrbitals(
inp["mo_kind"], norba, norbb, inp['mo_occs'], inp['mo_coeffs'],
inp['mo_energies'], None
)
# check number of electrons
if mo.nelec != inp['Naelec'] + inp['Nbelec']:
raise ValueError(f"Number of electrons in MolecularOrbitals is not equal to the sum of "
f"'Naelec' and 'Nbelec'. {mo.nelec} != {inp['Naelec']} + {inp['Nbelec']}")
if mo.occsa.sum() != inp['Naelec']:
raise ValueError(f"Number of alpha electrons in MolecularOrbitals is not equal to the "
f"'Naelec'. {mo.occsa.sum()} != {inp['Naelec']}")
if mo.occsb.sum() != inp['Nbelec']:
raise ValueError(f"Number of beta electrons in MolecularOrbitals is not equal to the "
f"'Nbelec'. {mo.occsb.sum()} != {inp['Nbelec']}")

return {
'title': inp['title'],
'atcoords': inp['atcoords'],
'atnums': inp['atnums'],
'atcorenums': inp['atcorenums'],
'obasis': obasis,
'mo': mo,
'energy': inp['E_tot'],
'extra': extra,
}
6 changes: 4 additions & 2 deletions iodata/orbitals.py
Expand Up @@ -38,9 +38,11 @@ class MolecularOrbitals:
kind
Type of molecular orbitals, which can be 'restricted', 'unrestricted', or 'generalized'.
norba
Number of alpha molecular orbitals, set to `None` in case of type=='generalized'.
Number of (occupied and virtual) alpha molecular orbitals.
Set to `None` in case oftype=='generalized'.
norbb
Number of beta molecular orbitals, set to `None` in case of type=='generalized'.
Number of (occupied and virtual) beta molecular orbitals.
Set to `None` in case of type=='generalized'.
This is expected to be equal to `norba` for the `restricted` kind.
occs
Molecular orbital occupation numbers. The length equals the number of columns of coeffs.
Expand Down