diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ca7e1d8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +sf_ip_ea/_static_version.py export-subst \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 12dd132..a170463 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ install: - sudo apt-get update # We do this conditionally because it saves us some downloading if the # version is the same. - - wget https://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh -O miniconda.sh; + - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; - bash miniconda.sh -b -p $HOME/miniconda - export PATH="$HOME/miniconda/bin:$PATH" - conda config --set always_yes yes --set changeps1 no diff --git a/LICENSE.txt b/LICENSE.txt index e69de29..01d6bcb 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2019-2021, Shannon E. Houck +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/setup.py b/setup.py index eb7edcd..059a29a 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,49 @@ from distutils.core import setup +import setuptools + + +def get_version_and_cmdclass(package_path): + """Load version.py module without importing the whole package. + + Template code from miniver + """ + import os + from importlib.util import module_from_spec, spec_from_file_location + + spec = spec_from_file_location("version", os.path.join(package_path, "_version.py")) + module = module_from_spec(spec) + spec.loader.exec_module(module) + return module.__version__, module.cmdclass + + +version, cmdclass = get_version_and_cmdclass("sf_ip_ea") + setup( name='sf_ip_ea', - version='0.1.0', + version=version, + cmdclass=cmdclass, author='Shannon E. Houck', author_email='shouck@vt.edu', - packages=['sf_ip_ea', 'sf_ip_ea.test'], + packages=setuptools.find_packages(), + include_package_data=True, scripts=[], url='', license='LICENSE.txt', + install_requires=[ + "numpy>=1.7", + "scipy", + ], + extras_require={ + "tests": ["pytest",], + }, + tests_require=["pytest",], + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "Programming Language :: Python :: 3", + ], + zip_safe=False, description='Stand-alone RAS-SF-IP/EA code.', long_description=open('README.md').read(), ) diff --git a/sf_ip_ea/__init__.py b/sf_ip_ea/__init__.py index 1a91319..edcf8bb 100644 --- a/sf_ip_ea/__init__.py +++ b/sf_ip_ea/__init__.py @@ -119,3 +119,4 @@ def fock_ci(delta_a, delta_b, mol, conf_space="", ref_opts={}, sf_opts={}, print("ERROR: Program %s is not yet supported. Exiting..." %(program) ) exit() +from ._version import __version__ \ No newline at end of file diff --git a/sf_ip_ea/_static_version.py b/sf_ip_ea/_static_version.py new file mode 100644 index 0000000..5557f9b --- /dev/null +++ b/sf_ip_ea/_static_version.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# This file is part of 'miniver': https://github.com/jbweston/miniver +# +# This file will be overwritten by setup.py when a source or binary +# distribution is made. The magic value "__use_git__" is interpreted by +# version.py. + +version = "__use_git__" + +# These values are only set if the distribution was created with 'git archive' +refnames = "$Format:%D$" +git_hash = "$Format:%h$" diff --git a/sf_ip_ea/_version.py b/sf_ip_ea/_version.py new file mode 100644 index 0000000..9969a7d --- /dev/null +++ b/sf_ip_ea/_version.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- +# This file is part of 'miniver': https://github.com/jbweston/miniver +# +from collections import namedtuple +import os +import subprocess + +from setuptools.command.build_py import build_py as build_py_orig +from setuptools.command.sdist import sdist as sdist_orig + +Version = namedtuple("Version", ("release", "dev", "labels")) + +# No public API +__all__ = [] + +package_root = os.path.dirname(os.path.realpath(__file__)) +package_name = os.path.basename(package_root) +distr_root = os.path.dirname(package_root) +# If the package is inside a "src" directory the +# distribution root is 1 level up. +if os.path.split(distr_root)[1] == "src": + _package_root_inside_src = True + distr_root = os.path.dirname(distr_root) +else: + _package_root_inside_src = False + +STATIC_VERSION_FILE = "_static_version.py" + + +def get_version(version_file=STATIC_VERSION_FILE): + version_info = get_static_version_info(version_file) + version = version_info["version"] + if version == "__use_git__": + version = get_version_from_git() + if not version: + version = get_version_from_git_archive(version_info) + if not version: + version = Version("unknown", None, None) + return pep440_format(version) + else: + return version + + +def get_static_version_info(version_file=STATIC_VERSION_FILE): + version_info = {} + with open(os.path.join(package_root, version_file), "rb") as f: + exec(f.read(), {}, version_info) + return version_info + + +def version_is_from_git(version_file=STATIC_VERSION_FILE): + return get_static_version_info(version_file)["version"] == "__use_git__" + + +def pep440_format(version_info): + release, dev, labels = version_info + + version_parts = [release] + if dev: + if release.endswith("-dev") or release.endswith(".dev"): + version_parts.append(dev) + else: # prefer PEP440 over strict adhesion to semver + version_parts.append(".dev{}".format(dev)) + + if labels: + version_parts.append("+") + version_parts.append(".".join(labels)) + + return "".join(version_parts) + + +def get_version_from_git(): + try: + p = subprocess.Popen( + ["git", "rev-parse", "--show-toplevel"], + cwd=distr_root, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + except OSError: + return + if p.wait() != 0: + return + if not os.path.samefile(p.communicate()[0].decode().rstrip("\n"), distr_root): + # The top-level directory of the current Git repository is not the same + # as the root directory of the distribution: do not extract the + # version from Git. + return + + # git describe --first-parent does not take into account tags from branches + # that were merged-in. The '--long' flag gets us the 'dev' version and + # git hash, '--always' returns the git hash even if there are no tags. + for opts in [["--first-parent"], []]: + try: + p = subprocess.Popen( + ["git", "describe", "--long", "--always"] + opts, + cwd=distr_root, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + except OSError: + return + if p.wait() == 0: + break + else: + return + + description = ( + p.communicate()[0] + .decode() + .strip("v") # Tags can have a leading 'v', but the version should not + .rstrip("\n") + .rsplit("-", 2) # Split the latest tag, commits since tag, and hash + ) + + try: + release, dev, git = description + except ValueError: # No tags, only the git hash + # prepend 'g' to match with format returned by 'git describe' + git = "g{}".format(*description) + release = "unknown" + dev = None + + labels = [] + if dev == "0": + dev = None + else: + labels.append(git) + + try: + p = subprocess.Popen(["git", "diff", "--quiet"], cwd=distr_root) + except OSError: + labels.append("confused") # This should never happen. + else: + if p.wait() == 1: + labels.append("dirty") + + return Version(release, dev, labels) + + +# TODO: change this logic when there is a git pretty-format +# that gives the same output as 'git describe'. +# Currently we can only tell the tag the current commit is +# pointing to, or its hash (with no version info) +# if it is not tagged. +def get_version_from_git_archive(version_info): + try: + refnames = version_info["refnames"] + git_hash = version_info["git_hash"] + except KeyError: + # These fields are not present if we are running from an sdist. + # Execution should never reach here, though + return None + + if git_hash.startswith("$Format") or refnames.startswith("$Format"): + # variables not expanded during 'git archive' + return None + + VTAG = "tag: v" + refs = set(r.strip() for r in refnames.split(",")) + version_tags = set(r[len(VTAG) :] for r in refs if r.startswith(VTAG)) + if version_tags: + release, *_ = sorted(version_tags) # prefer e.g. "2.0" over "2.0rc1" + return Version(release, dev=None, labels=None) + else: + return Version("unknown", dev=None, labels=["g{}".format(git_hash)]) + + +__version__ = get_version() + + +# The following section defines a module global 'cmdclass', +# which can be used from setup.py. The 'package_name' and +# '__version__' module globals are used (but not modified). + + +def _write_version(fname): + # This could be a hard link, so try to delete it first. Is there any way + # to do this atomically together with opening? + try: + os.remove(fname) + except OSError: + pass + with open(fname, "w") as f: + f.write( + "# This file has been created by setup.py.\n" + "version = '{}'\n".format(__version__) + ) + + +class _build_py(build_py_orig): + def run(self): + super().run() + _write_version(os.path.join(self.build_lib, package_name, STATIC_VERSION_FILE)) + + +class _sdist(sdist_orig): + def make_release_tree(self, base_dir, files): + super().make_release_tree(base_dir, files) + if _package_root_inside_src: + p = os.path.join("src", package_name) + else: + p = package_name + _write_version(os.path.join(base_dir, p, STATIC_VERSION_FILE)) + + +cmdclass = dict(sdist=_sdist, build_py=_build_py) diff --git a/sf_ip_ea/test/__init__.py b/sf_ip_ea/test/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/sf_ip_ea/test/__init__.py @@ -0,0 +1 @@ + diff --git a/sf_ip_ea/test/conftest.py b/sf_ip_ea/test/conftest.py new file mode 100644 index 0000000..84615cf --- /dev/null +++ b/sf_ip_ea/test/conftest.py @@ -0,0 +1,21 @@ +import pytest + + +def pytest_configure(config): + # Register marks to avoid warnings in installed testing + # sync with setup.cfg + config.addinivalue_line("markers", "blochtest") + config.addinivalue_line("markers", "davidsontest") + config.addinivalue_line("markers", "dftest") + config.addinivalue_line("markers", "frozentest") + config.addinivalue_line("markers", "methodtest") + + +@pytest.fixture(scope="function", autouse=True) +def set_up(): + import psi4 + + psi4.core.clean() + # psi4.core.clean_timers() # uncomment when v1.4 is lowest supported + psi4.core.clean_options() + psi4.set_output_file("pytest_output.dat", True) diff --git a/sf_ip_ea/test/pytest.ini b/sf_ip_ea/test/pytest.ini index c8ea93c..003e844 100644 --- a/sf_ip_ea/test/pytest.ini +++ b/sf_ip_ea/test/pytest.ini @@ -3,3 +3,5 @@ markers = methodtest: test the SF-IP/EA methods for correct energy values blochtest: test the Bloch effective Hamiltonian build frozentest: test frozen core/virtual + dftest + davidsontest diff --git a/sf_ip_ea/test/test_1sf/__init__.py b/sf_ip_ea/test/test_1sf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_ip_ea/test/test_1sf_ea/__init__.py b/sf_ip_ea/test/test_1sf_ea/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_ip_ea/test/test_1sf_ip/__init__.py b/sf_ip_ea/test/test_1sf_ip/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_ip_ea/test/test_2sf/__init__.py b/sf_ip_ea/test/test_2sf/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_ip_ea/test/test_bloch/__init__.py b/sf_ip_ea/test/test_bloch/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_ip_ea/test/test_davidson/__init__.py b/sf_ip_ea/test/test_davidson/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_ip_ea/test/test_df/__init__.py b/sf_ip_ea/test/test_df/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_ip_ea/test/test_ea/__init__.py b/sf_ip_ea/test/test_ea/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_ip_ea/test/test_frozens/__init__.py b/sf_ip_ea/test/test_frozens/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_ip_ea/test/test_ip/__init__.py b/sf_ip_ea/test/test_ip/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sf_ip_ea/test/test_ip/test_h_ip.py b/sf_ip_ea/test/test_ip/test_h_ip.py index e18d7a5..3c787c5 100644 --- a/sf_ip_ea/test/test_ip/test_h_ip.py +++ b/sf_ip_ea/test/test_ip/test_h_ip.py @@ -1,6 +1,8 @@ import psi4 import sf_ip_ea import pytest +from qcelemental.testing import compare_values + # threshold for value equality threshold = 1e-7 @@ -27,34 +29,27 @@ # Test: 1SF-CAS @pytest.mark.methodtest def test_1(): - psi4.core.clean() - psi4.core.clean_options() - psi4.core.clean_variables() options = {"basis": "cc-pvdz", 'num_roots': 4, 'diis_start': 20, 'e_convergence': 1e-10, 'd_convergence': 1e-10} expected = [-108.257015341635110, -108.220329170163453, -108.220329170162785, -108.199753179193195] wfn = sf_ip_ea.fock_ci( 1, 0, n2_7, conf_space="h", ref_opts=options, sf_opts={'NUM_ROOTS': 4} ) - for i, true in enumerate(wfn.e): - assert abs(true - expected[i]) < threshold + for i, ene in enumerate(wfn.e): + assert compare_values(expected[i], ene, atol=threshold) + @pytest.mark.methodtest def test_2(): - psi4.core.clean() - psi4.core.clean_options() - psi4.core.clean_variables() options = {"basis": "cc-pvdz", 'num_roots': 4, 'diis_start': 20, 'e_convergence': 1e-10, 'd_convergence': 1e-10} expected = [-108.299904314302950, -108.247296829002906, -108.236785088491118, -108.236785088490976] wfn = sf_ip_ea.fock_ci( 1, 0, n2_3, conf_space="h", ref_opts=options, sf_opts={'NUM_ROOTS': 8} ) for i, exp in enumerate(expected): - assert abs(exp - wfn.e[i]) < threshold + assert compare_values(expected[i], wfn.e[i], atol=threshold) + @pytest.mark.methodtest def test_3(): - psi4.core.clean() - psi4.core.clean_options() - psi4.core.clean_variables() options = {"basis": "cc-pvdz", 'e_convergence': 1e-10, 'd_convergence': 1e-10, 'diag_method': 'rsp'} expected = [-149.083861036268075, -149.083861036267962] wfn = sf_ip_ea.fock_ci( 1, 0, o2, conf_space="h", ref_opts=options, sf_opts={'NUM_ROOTS': 2} ) - for i, true in enumerate(wfn.e): - assert abs(true - expected[i]) < threshold + for i, ene in enumerate(wfn.e): + assert compare_values(expected[i], ene, atol=threshold)