diff --git a/iodata/__main__.py b/iodata/__main__.py
index e8228ffe..4f4a097c 100755
--- a/iodata/__main__.py
+++ b/iodata/__main__.py
@@ -113,9 +113,9 @@ def convert(infn, outfn, many, infmt, outfmt):
"""
if many:
- dump_many((data for data in load_many(infn, infmt)), outfn, outfmt)
+ dump_many(load_many(infn, fmt=infmt), outfn, fmt=outfmt)
else:
- dump_one(load_one(infn, infmt), outfn, outfmt)
+ dump_one(load_one(infn, fmt=infmt), outfn, fmt=outfmt)
def main():
diff --git a/iodata/api.py b/iodata/api.py
index 2669df2f..0936f3db 100644
--- a/iodata/api.py
+++ b/iodata/api.py
@@ -247,7 +247,14 @@ def _check_required(filename: str, iodata: IOData, dump_func: Callable):
@_reissue_warnings
-def dump_one(iodata: IOData, filename: str, fmt: Optional[str] = None, **kwargs):
+def dump_one(
+ iodata: IOData,
+ filename: str,
+ *,
+ fmt: Optional[str] = None,
+ allow_changes: bool = False,
+ **kwargs,
+):
"""Write data to a file.
This routine uses the extension or prefix of the filename to determine
@@ -263,25 +270,35 @@ def dump_one(iodata: IOData, filename: str, fmt: Optional[str] = None, **kwargs)
fmt
The name of the file format module to use. When not given, it is guessed
from the filename.
+ allow_changes
+ Whether conversion of the IOData object to a compatible form is allowed or not.
**kwargs
Keyword arguments are passed on to the format-specific dump_one function.
+ Returns
+ -------
+ data
+ The given ``IOData`` object or a shallow copy with some new attributes if converted.
+
Raises
------
DumpError
When an error is encountered while dumping to a file.
If the output file already existed, it is (partially) overwritten.
PrepareDumpError
- When the iodata object is not compatible with the file format,
- e.g. due to missing attributes, and not conversion is available or allowed
+ When the ``IOData`` object is not compatible with the file format,
+ e.g. due to missing attributes, and no conversion is available or allowed
to make it compatible.
If the output file already existed, it is not overwritten.
+ PrepareDumpWarning
+ When the ``IOData`` object is not compatible with the file format,
+ but it was converted to fix the compatibility issue.
"""
format_module = _select_format_module(filename, "dump_one", fmt)
try:
_check_required(filename, iodata, format_module.dump_one)
if hasattr(format_module, "prepare_dump"):
- format_module.prepare_dump(filename, iodata)
+ iodata = format_module.prepare_dump(iodata, allow_changes, filename)
except PrepareDumpError:
raise
except Exception as exc:
@@ -295,10 +312,18 @@ def dump_one(iodata: IOData, filename: str, fmt: Optional[str] = None, **kwargs)
raise
except Exception as exc:
raise DumpError("Uncaught exception while dumping to a file", filename) from exc
+ return iodata
@_reissue_warnings
-def dump_many(iodatas: Iterable[IOData], filename: str, fmt: Optional[str] = None, **kwargs):
+def dump_many(
+ iodatas: Iterable[IOData],
+ filename: str,
+ *,
+ fmt: Optional[str] = None,
+ allow_changes: bool = False,
+ **kwargs,
+):
"""Write multiple IOData instances to a file.
This routine uses the extension or prefix of the filename to determine
@@ -313,6 +338,8 @@ def dump_many(iodatas: Iterable[IOData], filename: str, fmt: Optional[str] = Non
The file to write the data to.
fmt
The name of the file format module to use.
+ allow_changes
+ Whether conversion of the IOData object to a compatible form is allowed or not.
**kwargs
Keyword arguments are passed on to the format-specific dump_many function.
@@ -322,12 +349,15 @@ def dump_many(iodatas: Iterable[IOData], filename: str, fmt: Optional[str] = Non
When an error is encountered while dumping to a file.
If the output file already existed, it (partially) overwritten.
PrepareDumpError
- When the iodata object is not compatible with the file format,
- e.g. due to missing attributes, and not conversion is available or allowed
+ When an ``IOData`` object is not compatible with the file format,
+ e.g. due to missing attributes, and no conversion is available or allowed
to make it compatible.
If the output file already existed, it is not overwritten when this error
- is raised while processing the first IOData instance in the ``iodatas`` argument.
+ is raised while processing the first ``IOData`` instance in the ``iodatas`` argument.
When the exception is raised in later iterations, any existing file is overwritten.
+ PrepareDumpWarning
+ When an ``IOData`` object is not compatible with the file format,
+ but it was converted to fix the compatibility issue.
"""
format_module = _select_format_module(filename, "dump_many", fmt)
@@ -342,7 +372,7 @@ def dump_many(iodatas: Iterable[IOData], filename: str, fmt: Optional[str] = Non
try:
_check_required(filename, first, format_module.dump_many)
if hasattr(format_module, "prepare_dump"):
- format_module.prepare_dump(filename, first)
+ first = format_module.prepare_dump(first, allow_changes, filename)
except PrepareDumpError:
raise
except Exception as exc:
@@ -356,9 +386,11 @@ def checking_iterator():
yield first
for other in iter_iodatas:
_check_required(filename, other, format_module.dump_many)
- if hasattr(format_module, "prepare_dump"):
- format_module.prepare_dump(filename, other)
- yield other
+ yield (
+ format_module.prepare_dump(other, allow_changes, filename)
+ if hasattr(format_module, "prepare_dump")
+ else other
+ )
with open(filename, "w") as f:
try:
diff --git a/iodata/formats/fchk.py b/iodata/formats/fchk.py
index e35f91f5..edc8b525 100644
--- a/iodata/formats/fchk.py
+++ b/iodata/formats/fchk.py
@@ -542,15 +542,28 @@ def _dump_real_arrays(name: str, val: NDArray[float], f: TextIO):
k = 0
-def prepare_dump(filename: str, data: IOData):
+def prepare_dump(data: IOData, allow_changes: bool, filename: str) -> IOData:
"""Check the compatibility of the IOData object with the FCHK format.
Parameters
----------
+ data
+ The IOData instance to be checked.
+ allow_changes
+ Whether conversion of the IOData object to a compatible form is allowed or not.
+ (not relevant for FCHK, present for API consistency)
filename
The file to be written to, only used for error messages.
+
+ Returns
+ -------
data
- The IOData instance to be checked.
+ The given ``IOData`` object.
+
+ Raises
+ ------
+ PrepareDumpError
+ If the given ``IOData`` instance is not compatible with the WFN format.
"""
if data.mo is not None:
if data.mo.kind == "generalized":
@@ -569,6 +582,7 @@ def prepare_dump(filename: str, data: IOData):
"followed by fully virtual ones.",
filename,
)
+ return data
@document_dump_one(
diff --git a/iodata/formats/json.py b/iodata/formats/json.py
index 019ecd18..b882f289 100644
--- a/iodata/formats/json.py
+++ b/iodata/formats/json.py
@@ -1446,16 +1446,28 @@ def _parse_provenance(
return base_provenance
-def prepare_dump(filename: str, data: IOData):
+def prepare_dump(data: IOData, allow_changes: bool, filename: str) -> IOData:
"""Check the compatibility of the IOData object with QCScheme.
Parameters
----------
+ data
+ The IOData instance to be checked.
+ allow_changes
+ Whether conversion of the IOData object to a compatible form is allowed or not.
+ (not relevant for QCSchema JSON, present for API consistency)
filename
The file to be written to, only used for error messages.
+
+ Returns
+ -------
data
- The IOData instance to be checked.
+ The given ``IOData`` object.
+ Raises
+ ------
+ PrepareDumpError
+ If the given ``IOData`` instance is not compatible with the WFN format.
"""
if "schema_name" not in data.extra:
raise PrepareDumpError(
@@ -1464,6 +1476,7 @@ def prepare_dump(filename: str, data: IOData):
schema_name = data.extra["schema_name"]
if schema_name == "qcschema_basis":
raise PrepareDumpError(f"{schema_name} not yet implemented in IOData.", filename)
+ return data
@document_dump_one(
diff --git a/iodata/formats/molden.py b/iodata/formats/molden.py
index 14528094..495a259f 100644
--- a/iodata/formats/molden.py
+++ b/iodata/formats/molden.py
@@ -46,6 +46,7 @@
from ..orbitals import MolecularOrbitals
from ..overlap import compute_overlap, gob_cart_normalization
from ..periodic import num2sym, sym2num
+from ..prepare import prepare_unrestricted_aminusb
from ..utils import DumpError, LineIterator, LoadError, LoadWarning, PrepareDumpError, angstrom
__all__ = []
@@ -768,24 +769,37 @@ def _fix_molden_from_buggy_codes(result: dict, lit: LineIterator, norm_threshold
)
-def prepare_dump(filename: str, data: IOData):
+def prepare_dump(data: IOData, allow_changes: bool, filename: str) -> IOData:
"""Check the compatibility of the IOData object with the Molden format.
Parameters
----------
+ data
+ The IOData instance to be checked.
+ allow_changes
+ Whether conversion of the IOData object to a compatible form is allowed or not.
filename
The file to be written to, only used for error messages.
+
+ Returns
+ -------
data
- The IOData instance to be checked.
+ The given ``IOData`` object or a shallow copy with some new attributes.
+
+ Raises
+ ------
+ PrepareDumpError
+ If the given ``IOData`` instance is not compatible with the WFN format.
+ PrepareDumpWarning
+ If the a converted ``IOData`` instance is returned.
"""
if data.mo is None:
raise PrepareDumpError("The Molden format requires molecular orbitals.", filename)
if data.obasis is None:
raise PrepareDumpError("The Molden format requires an orbital basis set.", filename)
- if data.mo.occs_aminusb is not None:
- raise PrepareDumpError("Cannot write Molden file when mo.occs_aminusb is set.", filename)
if data.mo.kind == "generalized":
raise PrepareDumpError("Cannot write Molden file with generalized orbitals.", filename)
+ return prepare_unrestricted_aminusb(data, allow_changes, filename, "Molden")
@document_dump_one("Molden", ["atcoords", "atnums", "mo", "obasis"], ["atcorenums", "title"])
diff --git a/iodata/formats/molekel.py b/iodata/formats/molekel.py
index c6c3e353..cf57d0bd 100644
--- a/iodata/formats/molekel.py
+++ b/iodata/formats/molekel.py
@@ -24,6 +24,7 @@
"""
from typing import TextIO
+from warnings import warn
import numpy as np
from numpy.typing import NDArray
@@ -32,7 +33,8 @@
from ..docstrings import document_dump_one, document_load_one
from ..iodata import IOData
from ..orbitals import MolecularOrbitals
-from ..utils import DumpError, LineIterator, LoadError, PrepareDumpError, angstrom
+from ..prepare import prepare_unrestricted_aminusb
+from ..utils import DumpError, LineIterator, LoadError, LoadWarning, PrepareDumpError, angstrom
from .molden import CONVENTIONS, _fix_molden_from_buggy_codes
__all__ = []
@@ -235,7 +237,16 @@ def load_one(lit: LineIterator, norm_threshold: float = 1e-4) -> dict:
)
nalpha = int(np.round(occsa.sum()))
nbeta = int(np.round(occsb.sum()))
- assert abs(spinpol - abs(nalpha - nbeta)) < 1e-7
+ if abs(spinpol - abs(nalpha - nbeta)) > 1e-7:
+ warn(
+ LoadWarning(
+ f"The spin polarization ({spinpol}) is inconsistent with the"
+ f"difference between alpha and beta occupation numbers ({nalpha} - {nbeta}). "
+ "The spin polarization will be rederived from the occupation numbers.",
+ lit,
+ ),
+ stacklevel=2,
+ )
assert nelec == nalpha + nbeta
assert coeffsa.shape == coeffsb.shape
assert energiesa.shape == energiesb.shape
@@ -261,24 +272,37 @@ def load_one(lit: LineIterator, norm_threshold: float = 1e-4) -> dict:
return result
-def prepare_dump(filename: str, data: IOData):
+def prepare_dump(data: IOData, allow_changes: bool, filename: str) -> IOData:
"""Check the compatibility of the IOData object with the Molekel format.
Parameters
----------
+ data
+ The IOData instance to be checked.
+ allow_changes
+ Whether conversion of the IOData object to a compatible form is allowed or not.
filename
The file to be written to, only used for error messages.
+
+ Returns
+ -------
data
- The IOData instance to be checked.
+ The given ``IOData`` object or a shallow copy with some new attributes.
+
+ Raises
+ ------
+ PrepareDumpError
+ If the given ``IOData`` instance is not compatible with the WFN format.
+ PrepareDumpWarning
+ If the a converted ``IOData`` instance is returned.
"""
if data.mo is None:
raise PrepareDumpError("The Molekel format requires molecular orbitals.", filename)
if data.obasis is None:
raise PrepareDumpError("The Molekel format requires an orbital basis set.", filename)
- if data.mo.occs_aminusb is not None:
- raise PrepareDumpError("Cannot write Molekel file when mo.occs_aminusb is set.", filename)
if data.mo.kind == "generalized":
raise PrepareDumpError("Cannot write Molekel file with generalized orbitals.", filename)
+ return prepare_unrestricted_aminusb(data, allow_changes, filename, "Molekel")
@document_dump_one("Molekel", ["atcoords", "atnums", "mo", "obasis"], ["atcharges"])
diff --git a/iodata/formats/wfn.py b/iodata/formats/wfn.py
index 05fce829..99663c63 100644
--- a/iodata/formats/wfn.py
+++ b/iodata/formats/wfn.py
@@ -38,6 +38,7 @@
from ..orbitals import MolecularOrbitals
from ..overlap import gob_cart_normalization
from ..periodic import num2sym, sym2num
+from ..prepare import prepare_unrestricted_aminusb
from ..utils import LineIterator, LoadError, PrepareDumpError
__all__ = []
@@ -496,15 +497,29 @@ def _dump_helper_section(f: TextIO, data: NDArray, fmt: str, skip: int, step: in
DEFAULT_WFN_TTL = "WFN auto-generated by IOData"
-def prepare_dump(filename: str, data: IOData):
+def prepare_dump(data: IOData, allow_changes: bool, filename: str) -> IOData:
"""Check the compatibility of the IOData object with the WFN format.
Parameters
----------
+ data
+ The IOData instance to be checked.
+ allow_changes
+ Whether conversion of the IOData object to a compatible form is allowed or not.
filename
The file to be written to, only used for error messages.
+
+ Returns
+ -------
data
- The IOData instance to be checked.
+ The given ``IOData`` object or a shallow copy with some new attributes.
+
+ Raises
+ ------
+ PrepareDumpError
+ If the given ``IOData`` instance is not compatible with the WFN format.
+ PrepareDumpWarning
+ If the a converted ``IOData`` instance is returned.
"""
if data.mo is None:
raise PrepareDumpError("The WFN format requires molecular orbitals", filename)
@@ -512,13 +527,12 @@ def prepare_dump(filename: str, data: IOData):
raise PrepareDumpError("The WFN format requires an orbital basis set", filename)
if data.mo.kind == "generalized":
raise PrepareDumpError("Cannot write WFN file with generalized orbitals.", filename)
- if data.mo.occs_aminusb is not None:
- raise PrepareDumpError("Cannot write WFN file when mo.occs_aminusb is set.", filename)
for shell in data.obasis.shells:
if any(kind != "c" for kind in shell.kinds):
raise PrepareDumpError(
"The WFN format only supports Cartesian MolecularBasis.", filename
)
+ return prepare_unrestricted_aminusb(data, allow_changes, filename, "WFN")
@document_dump_one(
diff --git a/iodata/formats/wfx.py b/iodata/formats/wfx.py
index 2a15501f..81aa759a 100644
--- a/iodata/formats/wfx.py
+++ b/iodata/formats/wfx.py
@@ -32,6 +32,7 @@
from ..iodata import IOData
from ..orbitals import MolecularOrbitals
from ..periodic import num2sym
+from ..prepare import prepare_unrestricted_aminusb
from ..utils import LineIterator, LoadError, LoadWarning, PrepareDumpError
from .wfn import CONVENTIONS, build_obasis, get_mocoeff_scales
@@ -329,15 +330,29 @@ def load_one(lit: LineIterator) -> dict:
}
-def prepare_dump(filename: str, data: IOData):
+def prepare_dump(data: IOData, allow_changes: bool, filename: str) -> IOData:
"""Check the compatibility of the IOData object with the WFX format.
Parameters
----------
+ data
+ The IOData instance to be checked.
+ allow_changes
+ Whether conversion of the IOData object to a compatible form is allowed or not.
filename
The file to be written to, only used for error messages.
+
+ Returns
+ -------
data
- The IOData instance to be checked.
+ The given ``IOData`` object or a shallow copy with some new attributes.
+
+ Raises
+ ------
+ PrepareDumpError
+ If the given ``IOData`` instance is not compatible with the WFN format.
+ PrepareDumpWarning
+ If the a converted ``IOData`` instance is returned.
"""
if data.mo is None:
raise PrepareDumpError("The WFX format requires molecular orbitals.", filename)
@@ -345,13 +360,12 @@ def prepare_dump(filename: str, data: IOData):
raise PrepareDumpError("The WFX format requires an orbital basis set.", filename)
if data.mo.kind == "generalized":
raise PrepareDumpError("Cannot write WFX file with generalized orbitals.", filename)
- if data.mo.occs_aminusb is not None:
- raise PrepareDumpError("Cannot write WFX file when mo.occs_aminusb is set.", filename)
for shell in data.obasis.shells:
if any(kind != "c" for kind in shell.kinds):
raise PrepareDumpError(
"The WFX format only supports Cartesian MolecularBasis.", filename
)
+ return prepare_unrestricted_aminusb(data, allow_changes, filename, "WFX")
@document_dump_one(
diff --git a/iodata/orbitals.py b/iodata/orbitals.py
index beb5a4a8..5369ab8a 100644
--- a/iodata/orbitals.py
+++ b/iodata/orbitals.py
@@ -26,7 +26,7 @@
from .attrutils import convert_array_to, validate_shape
-__all__ = ["MolecularOrbitals"]
+__all__ = ("MolecularOrbitals", "convert_to_unrestricted")
def validate_norbab(mo, attribute, value):
@@ -200,7 +200,7 @@ def norb(self):
return None
@property
- def spinpol(self) -> float:
+ def spinpol(self) -> Optional[float]:
"""Return the spin polarization of the Slater determinant."""
if self.kind == "generalized":
raise NotImplementedError
@@ -381,3 +381,36 @@ def irrepsb(self):
if self.kind == "restricted":
return self.irreps
return self.irreps[self.norba :]
+
+
+def convert_to_unrestricted(mo: MolecularOrbitals) -> MolecularOrbitals:
+ """Convert orbitals to ``kind="unrestricted"``.
+
+ Parameters
+ ----------
+ mo
+ Restricted molecular orbitals to be converted.
+
+ Returns
+ -------
+ new_mo
+ The given object if the orbitals were already unrestricted or a new unrestricted copy.
+
+ Raises
+ ------
+ ValueError
+ When the given orbitals are generalized.
+ """
+ if mo.kind == "generalized":
+ raise ValueError("Generalized orbitals cannot be converted to unrestricted.")
+ if mo.kind == "unrestricted":
+ return mo
+ return MolecularOrbitals(
+ "unrestricted",
+ mo.norba,
+ mo.norbb,
+ None if mo.occs is None else np.concatenate([mo.occsa, mo.occsb]),
+ None if mo.coeffs is None else np.concatenate([mo.coeffs, mo.coeffs], axis=1),
+ None if mo.energies is None else np.concatenate([mo.energies, mo.energies]),
+ None if mo.irreps is None else np.concatenate([mo.irreps, mo.irreps]),
+ )
diff --git a/iodata/prepare.py b/iodata/prepare.py
new file mode 100644
index 00000000..12958893
--- /dev/null
+++ b/iodata/prepare.py
@@ -0,0 +1,94 @@
+# 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
+# --
+"""Preparation of IOData instances before they are dumped.
+
+The ``prepare_*`` functions below can be used as building blocks in ``prepare_dump`` functions.
+When the ``allow_changes`` argument is set to ``True``,
+they can return a new IOData instance with some modified attributes.
+Otherwise, they can raise an error if a conversion is needed.
+When no conversion is needed, no errors or warnings are raised,
+and the same IOData object is returned.
+"""
+
+from warnings import warn
+
+import attrs
+
+from .iodata import IOData
+from .orbitals import convert_to_unrestricted
+from .utils import PrepareDumpError, PrepareDumpWarning
+
+__all__ = ("prepare_unrestricted_aminusb",)
+
+
+def prepare_unrestricted_aminusb(data: IOData, allow_changes: bool, filename: str, fmt: str):
+ """If molecular orbitals have aminusb set, they are converted to unrestricted.
+
+ Parameters
+ ----------
+ data
+ The IOData instance with the molecular orbitals.
+ allow_changes
+ Whether conversion of the IOData object to a compatible form is allowed or not.
+ filename
+ The file to be written to, only used for error messages.
+ fmt
+ The file format whose dump function is calling this function, only used for error messages.
+
+
+ Returns
+ -------
+ data
+ The given data object if no conversion took place,
+ or a shallow copy with some new attriubtes.
+
+ Raises
+ ------
+ ValueError
+ If the given data object has no molecular orbitals,
+ or if the orbitals are generalized.
+ PrepareDumpError
+ If ``allow_changes == False`` and a conversion is required.
+ PrepareDumpWarning
+ If ``allow_changes == True`` and a conversion is required.
+
+ """
+ # Check: possible, needed?
+ if data.mo is None:
+ raise ValueError("The given IOData instance has no molecular orbitals.")
+ if data.mo.kind == "generalized":
+ raise ValueError("prepare_unrestricted_aminusb is not applicable to generalized orbitals.")
+ if data.mo.kind == "unrestricted":
+ return data
+ if data.mo.occs_aminusb is None:
+ return data
+
+ # Raise error or warning
+ message = f"The {fmt} format does not support restricted orbitals with mo.occs_aminusb. "
+ if not allow_changes:
+ raise PrepareDumpError(
+ message + "Set allow_change=True to enable conversion to unrestricted.", filename
+ )
+ warn(
+ PrepareDumpWarning(message + "The orbitals are converted to unrestricted", filename),
+ stacklevel=2,
+ )
+
+ # Convert
+ return attrs.evolve(data, mo=convert_to_unrestricted(data.mo))
diff --git a/iodata/test/data/water_wrong_spinmult.mkl b/iodata/test/data/water_wrong_spinmult.mkl
new file mode 100644
index 00000000..747641e2
--- /dev/null
+++ b/iodata/test/data/water_wrong_spinmult.mkl
@@ -0,0 +1,91 @@
+$MKL
+#
+# A cooked-up MKL file to trigger a warning about spin
+#
+$CHAR_MULT
+ 2 4
+$END
+
+$COORD
+ 8 0.000000 0.000000 0.000000
+ 1 0.000000 0.000000 0.950000
+ 1 0.895670 0.000000 -0.316663
+$END
+
+$BASIS
+ 1 S 1.00
+ 130.7093210000 0.1543289670
+ 23.8088661000 0.5353281420
+ 6.4436083100 0.4446345420
+ 1 S 1.00
+ 5.0331513200 -0.0999672292
+ 1.1695961200 0.3995128260
+ 0.3803889600 0.7001154690
+ 3 P 1.00
+ 5.0331513200 0.1559162750
+ 1.1695961200 0.6076837190
+ 0.3803889600 0.3919573930
+$$
+ 1 S 1.00
+ 3.4252509100 0.1543289670
+ 0.6239137300 0.5353281420
+ 0.1688554040 0.4446345420
+$$
+ 1 S 1.00
+ 3.4252509100 0.1543289670
+ 0.6239137300 0.5353281420
+ 0.1688554040 0.4446345420
+
+$END
+
+$COEFF_ALPHA
+a1g a1g a1g a1g a1g
+ -20.233394200000 -1.265839420000 -0.629365088000 -0.441724988000 -0.387671783000
+ 0.994099882000 -0.232889095000 0.000000016550 0.100235366000 0.000000000000
+ 0.026779921300 0.831788042000 -0.000000090302 -0.523423149000 -0.000000000000
+ 0.003466300040 0.103349385000 -0.346565859000 0.648259144000 0.000000000000
+ 0.000000000000 -0.000000000000 0.000000000000 -0.000000000000 1.000000000000
+ 0.002451056010 0.073079409700 0.490116062000 0.458390414000 0.000000000000
+ -0.006083938420 0.160223990000 0.441542336000 0.269085788000 0.000000000000
+ -0.006083936930 0.160223948000 -0.441542341000 0.269085849000 0.000000000000
+a1g a1g
+ 0.603082408000 0.766134805000
+ -0.135631600000 0.000000056766
+ 0.908581133000 -0.000000429452
+ 0.583295647000 0.582525068000
+ 0.000000000000 -0.000000000000
+ 0.412453695000 -0.823811720000
+ -0.807337352000 0.842614916000
+ -0.807337875000 -0.842614243000
+ $END
+
+$OCC_ALPHA
+ 1.0000000 1.0000000 1.0000000 1.0000000 0.8500000
+ 0.6500000 0.0000000
+ $END
+
+$COEFF_BETA
+a1g a1g a1g a1g a1g
+ -20.233394200000 -1.265839420000 -0.629365088000 -0.441724988000 -0.387671783000
+ 0.994099882000 -0.232889095000 0.000000016550 0.100235366000 0.000000000000
+ 0.026779921300 0.831788042000 -0.000000090302 -0.523423149000 -0.000000000000
+ 0.003466300040 0.103349385000 -0.346565859000 0.648259144000 0.000000000000
+ 0.000000000000 -0.000000000000 0.000000000000 -0.000000000000 1.000000000000
+ 0.002451056010 0.073079409700 0.490116062000 0.458390414000 0.000000000000
+ -0.006083938420 0.160223990000 0.441542336000 0.269085788000 0.000000000000
+ -0.006083936930 0.160223948000 -0.441542341000 0.269085849000 0.000000000000
+a1g a1g
+ 0.603082408000 0.766134805000
+ -0.135631600000 0.000000056766
+ 0.908581133000 -0.000000429452
+ 0.583295647000 0.582525068000
+ 0.000000000000 -0.000000000000
+ 0.412453695000 -0.823811720000
+ -0.807337352000 0.842614916000
+ -0.807337875000 -0.842614243000
+ $END
+
+$OCC_BETA
+ 1.0000000 1.0000000 0.0000000 0.0000000 0.1500000
+ 0.3500000 0.0000000
+ $END
diff --git a/iodata/test/test_molekel.py b/iodata/test/test_molekel.py
index ed524ff5..014c73d5 100644
--- a/iodata/test/test_molekel.py
+++ b/iodata/test/test_molekel.py
@@ -29,7 +29,7 @@
from ..api import dump_one, load_one
from ..basis import convert_conventions
from ..overlap import compute_overlap
-from ..utils import PrepareDumpError, angstrom
+from ..utils import LoadWarning, PrepareDumpError, angstrom
from .common import (
check_orthonormal,
compare_mols,
@@ -170,3 +170,12 @@ def test_generalized():
data = create_generalized()
with pytest.raises(PrepareDumpError):
dump_one(data, "generalized.mkl")
+
+
+def test_load_wrong_spin_mult():
+ with (
+ as_file(files("iodata.test.data").joinpath("water_wrong_spinmult.mkl")) as fn_molekel,
+ pytest.warns(LoadWarning),
+ ):
+ data = load_one(fn_molekel)
+ assert_allclose(data.spinpol, 3)
diff --git a/iodata/test/test_orbitals.py b/iodata/test/test_orbitals.py
index fd2f845e..be06c9dd 100644
--- a/iodata/test/test_orbitals.py
+++ b/iodata/test/test_orbitals.py
@@ -22,7 +22,7 @@
import pytest
from numpy.testing import assert_allclose, assert_equal
-from ..orbitals import MolecularOrbitals
+from ..orbitals import MolecularOrbitals, convert_to_unrestricted
def test_wrong_kind():
@@ -518,3 +518,97 @@ def test_generalized_irreps():
_ = mo.irrepsa
with pytest.raises(NotImplementedError):
_ = mo.irrepsb
+
+
+def test_convert_to_unrestricted_generalized():
+ with pytest.raises(ValueError):
+ convert_to_unrestricted(MolecularOrbitals("generalized", None, None))
+
+
+def test_convert_to_unrestricted_pass_through():
+ mo1 = MolecularOrbitals("unrestricted", 5, 3, occs=[1, 1, 0, 0, 0, 1, 0, 0])
+ mo2 = convert_to_unrestricted(mo1)
+ assert mo1 is mo2
+
+
+def test_convert_to_unrestricted_minimal():
+ mo1 = MolecularOrbitals("restricted", 5, 5)
+ mo2 = convert_to_unrestricted(mo1)
+ assert mo1 is not mo2
+ assert mo2.kind == "unrestricted"
+ assert mo2.norba == 5
+ assert mo2.norbb == 5
+ assert mo2.coeffs is None
+ assert mo2.occs is None
+ assert mo2.coeffs is None
+ assert mo2.energies is None
+ assert mo2.irreps is None
+
+
+def test_convert_to_unrestricted_aminusb():
+ mo1 = MolecularOrbitals(
+ "restricted",
+ 5,
+ 5,
+ occs=np.array([2.0, 0.8, 0.2, 0.0, 0.0]),
+ occs_aminusb=np.array([0.0, 0.2, 0.2, 0.0, 0.0]),
+ )
+ assert_allclose(mo1.spinpol, 0.4)
+ mo2 = convert_to_unrestricted(mo1)
+ assert mo1 is not mo2
+ assert mo2.kind == "unrestricted"
+ assert_allclose(mo2.occsa, [1.0, 0.5, 0.2, 0.0, 0.0])
+ assert_allclose(mo2.occsb, [1.0, 0.3, 0.0, 0.0, 0.0])
+ assert_allclose(mo2.spinpol, 0.4)
+
+
+def test_convert_to_unrestricted_occ_integer():
+ mo1 = MolecularOrbitals(
+ "restricted",
+ 5,
+ 5,
+ occs=np.array([2.0, 1.0, 1.0, 0.0, 0.0]),
+ )
+ mo2 = convert_to_unrestricted(mo1)
+ assert mo1 is not mo2
+ assert mo2.kind == "unrestricted"
+ assert_allclose(mo2.occsa, [1.0, 1.0, 1.0, 0.0, 0.0])
+ assert_allclose(mo2.occsb, [1.0, 0.0, 0.0, 0.0, 0.0])
+
+
+def test_convert_to_unrestricted_occ_float():
+ mo1 = MolecularOrbitals(
+ "restricted",
+ 5,
+ 5,
+ occs=np.array([2.0, 1.6, 1.0, 0.0, 0.0]),
+ )
+ mo2 = convert_to_unrestricted(mo1)
+ assert mo1 is not mo2
+ assert mo2.kind == "unrestricted"
+ assert_allclose(mo2.occsa, [1.0, 0.8, 0.5, 0.0, 0.0])
+ assert_allclose(mo2.occsb, mo2.occsa)
+
+
+def test_convert_to_unrestricted_full():
+ rng = np.random.default_rng(42)
+ mo1 = MolecularOrbitals(
+ "restricted",
+ 5,
+ 5,
+ occs=rng.uniform(0, 1, 5),
+ coeffs=rng.uniform(0, 1, (8, 5)),
+ energies=rng.uniform(-1, 0, 5),
+ irreps=["A"] * 5,
+ )
+ mo2 = convert_to_unrestricted(mo1)
+ assert mo1 is not mo2
+ assert mo2.kind == "unrestricted"
+ assert_allclose(mo2.occsa, mo1.occsa)
+ assert_allclose(mo2.occsb, mo1.occsb)
+ assert_allclose(mo2.coeffsa, mo1.coeffsa)
+ assert_allclose(mo2.coeffsb, mo1.coeffsb)
+ assert_allclose(mo2.energiesa, mo1.energiesa)
+ assert_allclose(mo2.energiesb, mo1.energiesb)
+ assert_equal(mo2.irrepsa, mo1.irrepsa)
+ assert_equal(mo2.irrepsb, mo1.irrepsb)
diff --git a/iodata/test/test_prepare.py b/iodata/test/test_prepare.py
new file mode 100644
index 00000000..da2e443b
--- /dev/null
+++ b/iodata/test/test_prepare.py
@@ -0,0 +1,106 @@
+# 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
+# --
+"""Unit tests for iodata.prepare."""
+
+from importlib.resources import as_file, files
+from pathlib import Path
+
+import pytest
+from numpy.testing import assert_allclose, assert_equal
+
+from ..api import dump_one, load_one
+from ..iodata import IOData
+from ..orbitals import MolecularOrbitals
+from ..prepare import prepare_unrestricted_aminusb
+from ..utils import PrepareDumpError, PrepareDumpWarning
+
+
+def test_unrestricted_aminusb_no_mo():
+ data = IOData()
+ with pytest.raises(ValueError):
+ prepare_unrestricted_aminusb(data, False, "foo.wfn", "wfn")
+
+
+def test_unrestricted_aminusb_generaized():
+ data = IOData(mo=MolecularOrbitals("generalized", None, None))
+ with pytest.raises(ValueError):
+ prepare_unrestricted_aminusb(data, False, "foo.wfn", "wfn")
+
+
+def test_unrestricted_aminusb_pass_through1():
+ data1 = IOData(mo=MolecularOrbitals("unrestricted", 5, 3))
+ data2 = prepare_unrestricted_aminusb(data1, False, "foo.wfn", "wfn")
+ assert data1 is data2
+
+
+def test_unrestricted_aminusb_pass_through2():
+ data1 = IOData(mo=MolecularOrbitals("restricted", 4, 4, occs=[2, 1, 0, 0]))
+ data2 = prepare_unrestricted_aminusb(data1, False, "foo.wfn", "wfn")
+ assert data1 is data2
+
+
+def test_unrestricted_aminusb_pass_error():
+ data = IOData(
+ mo=MolecularOrbitals(
+ "restricted", 4, 4, occs=[2, 1, 0, 0], occs_aminusb=[0.7, 0.3, 0.0, 0.0]
+ )
+ )
+ with pytest.raises(PrepareDumpError):
+ prepare_unrestricted_aminusb(data, False, "foo.wfn", "wfn")
+
+
+def test_unrestricted_aminusb_pass_warning():
+ data1 = IOData(
+ atnums=[1, 1],
+ mo=MolecularOrbitals(
+ "restricted", 4, 4, occs=[2, 1, 0, 0], occs_aminusb=[0.7, 0.3, 0.0, 0.0]
+ ),
+ )
+ with pytest.warns(PrepareDumpWarning):
+ data2 = prepare_unrestricted_aminusb(data1, True, "foo.wfn", "wfn")
+ assert data1 is not data2
+ assert data1.atnums is data2.atnums
+ assert data1.mo is not data2.mo
+ assert data2.mo.kind == "unrestricted"
+ assert_equal(data2.mo.occsa, data1.mo.occsa)
+ assert_equal(data2.mo.occsb, data1.mo.occsb)
+
+
+@pytest.mark.parametrize("fmt", ["wfn", "wfx", "molden", "molekel"])
+def test_dump_occs_aminusb(tmpdir, fmt):
+ # Load a restricted spin-paired wfn and alter it.abs
+ with as_file(files("iodata.test.data").joinpath("water_sto3g_hf_g03.fchk")) as fn_fchk:
+ data1 = load_one(fn_fchk)
+ assert data1.mo.kind == "restricted"
+ data1.mo.occs = [2, 2, 2, 1, 1, 1, 0]
+ data1.mo.occs_aminusb = [0, 0, 0, 1, 0.3, 0.2, 0]
+ assert_allclose(data1.spinpol, data1.mo.spinpol)
+
+ # Dump and load again
+ path_foo = Path(tmpdir) / "foo"
+ with pytest.raises(PrepareDumpError):
+ dump_one(data1, path_foo, fmt=fmt)
+ with pytest.warns(PrepareDumpWarning):
+ dump_one(data1, path_foo, allow_changes=True, fmt=fmt)
+ data2 = load_one(path_foo, fmt=fmt)
+
+ # Check the loaded file
+ assert data2.mo.kind == "unrestricted"
+ assert_allclose(data2.mo.occsa, data1.mo.occsa)
+ assert_allclose(data2.mo.occsb, data1.mo.occsb)
diff --git a/iodata/utils.py b/iodata/utils.py
index def01638..54b61f83 100644
--- a/iodata/utils.py
+++ b/iodata/utils.py
@@ -38,6 +38,7 @@
"WriteInputError",
"LoadWarning",
"DumpWarning",
+ "PrepareDumpWarning",
"Cube",
"set_four_index_element",
"volume",
@@ -229,6 +230,10 @@ class DumpWarning(BaseFileWarning):
"""Raised when an IOData object is made compatible with a format when dumping to a file."""
+class PrepareDumpWarning(BaseFileWarning):
+ """Raised when an IOData object is made compatible with a format before dumping to a file."""
+
+
@attrs.define
class Cube:
"""The volumetric data from a cube (or similar) file."""