From 27666c47fafa26b6b87b62a3552439a4e453e424 Mon Sep 17 00:00:00 2001 From: zachcran <15938371+zachcran@users.noreply.github.com> Date: Wed, 29 Oct 2025 16:58:35 -0600 Subject: [PATCH 01/13] Update gitignore to use GitHub python ignores [no ci] --- .gitignore | 77 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index ed7bae9..23c79ec 100644 --- a/.gitignore +++ b/.gitignore @@ -16,31 +16,60 @@ *.tmp *.autosave *.bak +*~ +*.py[cod] +*.so +*.cfg +!.isort.cfg +!setup.cfg +*.orig +*.log +*.pot +__pycache__/* +.cache/* +.*.swp +*/.ipynb_checkpoints/* +.DS_Store # These are directories used by IDEs for storing settings -.idea/ -.vscode/ - -# These are common Python virtual enviornment directory names -venv/ -docs/venv/ - -# This is where Jupyter/IPython store backup files -.ipynb_checkpoints/ - -# Byte-compiled Python -__pycache__/ - -# These are common build directory names -build*/ -docs/build -docs/source/_build -docs/doxyoutput -docs/source/api -*-build-*/ -_build/ -Debug/ -Release/ +.ropeproject +.project +.pydevproject +.settings +.idea +.vscode +tags + +# Package files +*.egg +*.eggs/ +.installed.cfg +*.egg-info + +# Unittest and coverage +htmlcov/* +.coverage +.coverage.* +.tox +junit*.xml +coverage.xml +.pytest_cache/ + +# Build and docs folder/files +build/* +dist/* +sdist/* +docs/source/api/* +!docs/source/api/modules.rst +docs/source/_rst/* +docs/build/* +cover/* +MANIFEST + +# Per-project virtualenvs +*venv*/ +.conda*/ +.python-version # Users commonly store their specific CMake settings in a toolchain file toolchain.cmake @@ -50,4 +79,4 @@ cache.db uuid.db # This is a generated file -src/python/friendzone/friends.py +# src/python/friendzone/friends.py From b3232c326c9d0147462271e25a2bf9ad2e67ef06 Mon Sep 17 00:00:00 2001 From: zachcran <15938371+zachcran@users.noreply.github.com> Date: Wed, 29 Oct 2025 16:59:40 -0600 Subject: [PATCH 02/13] Define pip-installable python package that doesn't touch the cmake build --- pyproject.toml | 64 ++++++++++++++++++++++++++++++++ src/python/friendzone/friends.py | 59 +++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 pyproject.toml create mode 100644 src/python/friendzone/friends.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..618681d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,64 @@ +# This file defines the pip-installable Python package. + +[build-system] +requires = ["setuptools>=46.1.0", "setuptools_scm[toml]>=5"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +# For smarter version schemes and other configuration options, +# check out https://github.com/pypa/setuptools_scm +version_scheme = "no-guess-dev" + +# To create a pip-installable package that uses CMake in the backend, +# scikit-build-core is used as the build backend, recommended by Pybind11. +# +# scikit-build-core: https://scikit-build-core.readthedocs.io/en/latest/ +# Pybind11 scikit-build-core example: https://github.com/pybind/scikit_build_example +# [build-system] +# requires = ["scikit-build-core>=0.11", "pybind11>=3.0"] +# build-backend = "scikit_build_core.build" + + +[project] +name = "friendzone" +license = "Apache-2.0" +license-files = ["LICENSE"] +description = "A minimal example package (with pybind11)" +readme = "README.md" +authors = [{ name = "zachcran", email = "zachcran@iastate.edu" }] +requires-python = ">=3.10" +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Private :: Do Not Upload", +] +# Dynamic project attributes +# +# Set the project version dynamically according to git tags. +# Git tag version info: https://github.com/pypa/setuptools-scm/blob/fb261332d9b46aa5a258042d85baa5aa7b9f4fa2/README.rst#default-versioning-scheme +dynamic = ["version"] + + +[tool.setuptools] +package-dir = {"" = "src/python"} + + +[dependency-groups] +test = ["pytest"] +dev = [{ include-group = "test" }] +ase = ["ase"] +nwchem = ["qcengine", "qcelemental", "networkx"] + + +[tool.pytest.ini_options] +minversion = "8.0" +addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"] +xfail_strict = true +log_cli_level = "INFO" +filterwarnings = ["error", "ignore::pytest.PytestCacheWarning"] +testpaths = ["tests"] diff --git a/src/python/friendzone/friends.py b/src/python/friendzone/friends.py new file mode 100644 index 0000000..aa01b46 --- /dev/null +++ b/src/python/friendzone/friends.py @@ -0,0 +1,59 @@ +# Copyright 2025 NWChemEx-Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging + +logger = logging.getLogger(__name__) + + +def friends() -> dict[str, bool]: + """Returns a dictionary of potential friends and whether they were enabled. + + :return: Key-value pairs where the key is the name of a potential + friend and the value is whether that friend was enabled or not + """ + friends_list = {"ase": False, "nwchem": False} + + try: + import ase + + friends_list["ase"] = True + except ImportError: + logger.warning("Module not enabled: ase") + + try: + import networkx + import qcelemental + import qcengine + + friends_list["nwchem"] = True + except ImportError: + logger.warning("Module not enabled: nwchem") + + return friends_list + + +def is_friend_enabled(friend: str) -> bool: + """Wraps the process of determining if a particular friend was enabled. + + :return: True if FriendZone was configured with support for ``friend`` + and false otherwise. + :rtype: bool + """ + all_friends = friends() + + if friend in all_friends: + return all_friends[friend] + + return False From 81657afd0fd16e73b7d63c1d102ab5cec556f28d Mon Sep 17 00:00:00 2001 From: zachcran <15938371+zachcran@users.noreply.github.com> Date: Wed, 29 Oct 2025 17:01:27 -0600 Subject: [PATCH 03/13] Update docstrings --- src/python/friendzone/friends.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/python/friendzone/friends.py b/src/python/friendzone/friends.py index aa01b46..2a75e9c 100644 --- a/src/python/friendzone/friends.py +++ b/src/python/friendzone/friends.py @@ -22,6 +22,7 @@ def friends() -> dict[str, bool]: :return: Key-value pairs where the key is the name of a potential friend and the value is whether that friend was enabled or not + :rtype: dict[str, bool] """ friends_list = {"ase": False, "nwchem": False} @@ -47,6 +48,9 @@ def friends() -> dict[str, bool]: def is_friend_enabled(friend: str) -> bool: """Wraps the process of determining if a particular friend was enabled. + :param friend: Name of friend to check + :type friend: str + :return: True if FriendZone was configured with support for ``friend`` and false otherwise. :rtype: bool From 3903da56fc30b62d41c6ae3025eddc0650ed4705 Mon Sep 17 00:00:00 2001 From: zachcran <15938371+zachcran@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:01:14 -0700 Subject: [PATCH 04/13] Update project description --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 618681d..e7e873a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ version_scheme = "no-guess-dev" name = "friendzone" license = "Apache-2.0" license-files = ["LICENSE"] -description = "A minimal example package (with pybind11)" +description = "Provides SimDE compatible APIs so that NWChemEx can play nicely with its friends." readme = "README.md" authors = [{ name = "zachcran", email = "zachcran@iastate.edu" }] requires-python = ">=3.10" From 283f82790033290a7a4e8721096ed28b582df5ea Mon Sep 17 00:00:00 2001 From: zachcran <15938371+zachcran@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:12:47 -0700 Subject: [PATCH 05/13] Split dependency groups from optional dependencies, explaining difference --- pyproject.toml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e7e873a..9ecf3d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,13 +47,19 @@ dynamic = ["version"] [tool.setuptools] package-dir = {"" = "src/python"} +# Optional dependencies represent optional features that can be enabled +# during installation +# Example: pip install friendzone[ase] +[project.optional-dependencies] +ase = ["ase"] +nwchem = ["qcengine", "qcelemental", "networkx"] +# Dependency groups are optional dependencies that are not intented to appear +# after packaging, usually used to help with testing or development +# Example: pip install . --group dev [dependency-groups] test = ["pytest"] dev = [{ include-group = "test" }] -ase = ["ase"] -nwchem = ["qcengine", "qcelemental", "networkx"] - [tool.pytest.ini_options] minversion = "8.0" From 8178cc3ce20357745caf5608b138ce07f3baec28 Mon Sep 17 00:00:00 2001 From: zachcran <15938371+zachcran@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:15:47 -0700 Subject: [PATCH 06/13] Note invisible dependency and document reason for using package-dir --- pyproject.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9ecf3d3..9087967 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,10 +42,14 @@ classifiers = [ # Set the project version dynamically according to git tags. # Git tag version info: https://github.com/pypa/setuptools-scm/blob/fb261332d9b46aa5a258042d85baa5aa7b9f4fa2/README.rst#default-versioning-scheme dynamic = ["version"] +# NOTE: Invisible dependency for now until Python bindings at SimDE are +# packaged properly and available here +# dependencies = ["simde"] [tool.setuptools] -package-dir = {"" = "src/python"} +# Cannot automatically find the friendzone namespace so we set it here +package-dir = { "" = "src/python" } # Optional dependencies represent optional features that can be enabled # during installation From cd8585abeae0a338089db17b9e41af4f154d617f Mon Sep 17 00:00:00 2001 From: "Jonathan M. Waldrop" Date: Tue, 11 Nov 2025 13:27:04 -0600 Subject: [PATCH 07/13] Update and add tests for friend checking --- .licenserc.yaml | 1 - CMakeLists.txt | 16 ---------- cmake/friends.py.in | 40 ----------------------- pyproject.toml | 16 +++++++++- src/python/friendzone/friends.py | 42 +++++++------------------ tests/python/unit_tests/test_friends.py | 34 ++++++++++++++++++++ 6 files changed, 60 insertions(+), 89 deletions(-) delete mode 100644 cmake/friends.py.in create mode 100644 tests/python/unit_tests/test_friends.py diff --git a/.licenserc.yaml b/.licenserc.yaml index 3e2e6a5..635023b 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -25,7 +25,6 @@ header: - docs/source/bibliography/*.bib - docs/source/nitpick_exceptions - version.txt - - cmake/friends.py.in - build/ comment: never diff --git a/CMakeLists.txt b/CMakeLists.txt index c1415d8..f861e8f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,18 +27,9 @@ include(get_cmaize) include(nwx_cxx_api_docs) ### Files and Paths ### -set(friends_template "${CMAKE_CURRENT_LIST_DIR}/cmake/friends.py.in") set(python_src_directory "${CMAKE_CURRENT_LIST_DIR}/src/python") # # Doxygen docs -if("${ONLY_BUILD_DOCS}") - # If we are only building the docs, we need to produce friends.py here - configure_file( - "${friends_template}" - "${python_src_directory}/friendzone/friends.py" - @ONLY - ) -endif() nwx_cxx_api_docs("${CMAKE_CURRENT_SOURCE_DIR}/src" "${CMAKE_CURRENT_SOURCE_DIR}/include") ### Options ### @@ -72,13 +63,6 @@ set( include(ase) include(nwchem) -## Configure file with enabled friends ## -configure_file( - "${friends_template}" # Input file - "${python_src_directory}/friendzone/friends.py" # Output file - @ONLY # Only replace @ variables -) - #TOOD: Replace cmaize_add_library when it supports Python add_library(${PROJECT_NAME} INTERFACE) target_link_libraries(${PROJECT_NAME} INTERFACE simde ase nwchem) diff --git a/cmake/friends.py.in b/cmake/friends.py.in deleted file mode 100644 index 10ec625..0000000 --- a/cmake/friends.py.in +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2023 NWChemEx-Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This file is auto-generated by FriendZone's configure step and will be -# overwritten next time configure is run. - - -def friends(): - """ Returns a dictionary of potential friends and whether they were enabled. - - :return: Key-value pairs where the key is the name of a potential - friend and the value is whether that friend was enabled or not - """ - return {'nwchem' : '@ENABLE_NWCHEM@', - 'ase' : '@ENABLE_ASE@' } - - -def is_friend_enabled(friend): - """ Wraps the process of determining if a particular friend was enabled. - - :return: True if FriendZone was configured with support for ``friend`` - and false otherwise. - :rtype: bool - """ - all_friends = friends() - if friend in all_friends: - status = all_friends[friend] - return status == 'ON' or status == 'on' or status == 'TRUE' or \ - status == 'true' diff --git a/pyproject.toml b/pyproject.toml index 9087967..623bc25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,17 @@ +# Copyright 2025 NWChemEx-Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + # This file defines the pip-installable Python package. [build-system] @@ -9,7 +23,7 @@ build-backend = "setuptools.build_meta" # check out https://github.com/pypa/setuptools_scm version_scheme = "no-guess-dev" -# To create a pip-installable package that uses CMake in the backend, +# To create a pip-installable package that uses CMake in the backend, # scikit-build-core is used as the build backend, recommended by Pybind11. # # scikit-build-core: https://scikit-build-core.readthedocs.io/en/latest/ diff --git a/src/python/friendzone/friends.py b/src/python/friendzone/friends.py index 2a75e9c..0b23308 100644 --- a/src/python/friendzone/friends.py +++ b/src/python/friendzone/friends.py @@ -12,36 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -import logging +from importlib.util import find_spec -logger = logging.getLogger(__name__) +def friends() -> list[str]: + """Returns a list of potentially supported friends. -def friends() -> dict[str, bool]: - """Returns a dictionary of potential friends and whether they were enabled. - - :return: Key-value pairs where the key is the name of a potential - friend and the value is whether that friend was enabled or not - :rtype: dict[str, bool] + :return: A list of names of potentially supported friends. + :rtype: list[str] """ - friends_list = {"ase": False, "nwchem": False} - - try: - import ase - - friends_list["ase"] = True - except ImportError: - logger.warning("Module not enabled: ase") - - try: - import networkx - import qcelemental - import qcengine - - friends_list["nwchem"] = True - except ImportError: - logger.warning("Module not enabled: nwchem") - + friends_list = [ + "ase", + "nwchem", + ] return friends_list @@ -55,9 +38,6 @@ def is_friend_enabled(friend: str) -> bool: and false otherwise. :rtype: bool """ - all_friends = friends() - - if friend in all_friends: - return all_friends[friend] - + if friend in friends() and find_spec(friend) is not None: + return True return False diff --git a/tests/python/unit_tests/test_friends.py b/tests/python/unit_tests/test_friends.py new file mode 100644 index 0000000..a1cebf4 --- /dev/null +++ b/tests/python/unit_tests/test_friends.py @@ -0,0 +1,34 @@ +# Copyright 2025 NWChemEx-Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from importlib.util import find_spec + +from friendzone.friends import friends, is_friend_enabled + + +class TestFriends(unittest.TestCase): + def test_friends_list(self): + expected_friends = ["ase", "nwchem"] + actual_friends = friends() + self.assertCountEqual(actual_friends, expected_friends) + + def test_is_friend_enabled(self): + # For known friends, the result should match the system state + for friend in friends(): + enabled = is_friend_enabled(friend) + module_found = find_spec(friend) is not None + self.assertEqual(enabled, module_found) + # For unknown friends, the result should be False + self.assertFalse(is_friend_enabled("non_existent_friend")) From 356e584f3ef79a852e0363fa4f2189c46ee5922a Mon Sep 17 00:00:00 2001 From: "Jonathan M. Waldrop" Date: Tue, 11 Nov 2025 15:16:17 -0600 Subject: [PATCH 08/13] .licenserc.yaml fix for venv --- .licenserc.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.licenserc.yaml b/.licenserc.yaml index 635023b..ea83d21 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -26,5 +26,6 @@ header: - docs/source/nitpick_exceptions - version.txt - build/ + - venv/ comment: never From 4de180c57ab535d698e97a26c6142b1205dc13bd Mon Sep 17 00:00:00 2001 From: "Jonathan M. Waldrop" Date: Tue, 11 Nov 2025 16:06:47 -0600 Subject: [PATCH 09/13] Update src/python/friendzone/friends.py Co-authored-by: Zachery Crandall <15938371+zachcran@users.noreply.github.com> --- src/python/friendzone/friends.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/python/friendzone/friends.py b/src/python/friendzone/friends.py index 0b23308..a18e694 100644 --- a/src/python/friendzone/friends.py +++ b/src/python/friendzone/friends.py @@ -38,6 +38,4 @@ def is_friend_enabled(friend: str) -> bool: and false otherwise. :rtype: bool """ - if friend in friends() and find_spec(friend) is not None: - return True - return False + return friend in friends() and find_spec(friend) is not None From 6fa028691546ec6ca21b9ea1867cc985551f2d51 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Waldrop" Date: Tue, 11 Nov 2025 16:34:34 -0600 Subject: [PATCH 10/13] update pyproject.toml authors --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 623bc25..c00cd6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,10 @@ license = "Apache-2.0" license-files = ["LICENSE"] description = "Provides SimDE compatible APIs so that NWChemEx can play nicely with its friends." readme = "README.md" -authors = [{ name = "zachcran", email = "zachcran@iastate.edu" }] +authors = [ + { name = "zachcran", email = "zachcran@iastate.edu" }, + { name = "jwaldrop107", email = "jwaldrop@ameslab.gov" }, +] requires-python = ">=3.10" classifiers = [ "Development Status :: 4 - Beta", From 04f5dbfc1b311c98da7858564d69c98a830564e5 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Waldrop" Date: Tue, 11 Nov 2025 22:18:18 -0600 Subject: [PATCH 11/13] rework friends --- CMakeLists.txt | 4 +- cmake/molssi.cmake | 38 +++++++++++++++++ cmake/nwchem.cmake | 5 --- pyproject.toml | 2 +- src/python/friendzone/__init__.py | 9 ++-- src/python/friendzone/friends.py | 40 +++++++++++------- src/python/friendzone/nwx2ase/__init__.py | 28 +++++++------ .../friendzone/nwx2ase/nwchem_via_ase.py | 18 ++++++++ src/python/friendzone/nwx2molssi/__init__.py | 42 +++++++++++++++++++ .../call_qcengine.py | 4 +- .../chemical_system_conversions.py | 0 .../nwchem_via_molssi.py} | 39 ++++++++--------- .../system_via_molssi.py} | 7 ++-- .../test_chemical_system_conversions.py | 6 +-- .../unit_tests/nwx2ase/test_nwchem_via_ase.py | 10 ++--- .../__init__.py | 0 .../test_chemical_system_conversions.py | 21 +++++++--- .../test_nwchem_via_molssi.py} | 9 ++-- .../test_system_via_molssi.py | 4 ++ .../unit_tests/nwx2qcengine/__init__.py | 13 ------ tests/python/unit_tests/test_friends.py | 32 ++++++++------ 21 files changed, 221 insertions(+), 110 deletions(-) create mode 100644 cmake/molssi.cmake create mode 100644 src/python/friendzone/nwx2molssi/__init__.py rename src/python/friendzone/{nwx2qcengine => nwx2molssi}/call_qcengine.py (97%) rename src/python/friendzone/{nwx2qcelemental => nwx2molssi}/chemical_system_conversions.py (100%) rename src/python/friendzone/{nwx2qcengine/__init__.py => nwx2molssi/nwchem_via_molssi.py} (81%) rename src/python/friendzone/{nwx2qcelemental/__init__.py => nwx2molssi/system_via_molssi.py} (91%) rename tests/python/unit_tests/{nwx2qcelemental => nwx2molssi}/__init__.py (100%) rename tests/python/unit_tests/{nwx2qcelemental => nwx2molssi}/test_chemical_system_conversions.py (73%) rename tests/python/unit_tests/{nwx2qcengine/test_nwchem.py => nwx2molssi/test_nwchem_via_molssi.py} (90%) rename tests/python/unit_tests/{nwx2qcelemental => nwx2molssi}/test_system_via_molssi.py (89%) delete mode 100644 tests/python/unit_tests/nwx2qcengine/__init__.py diff --git a/CMakeLists.txt b/CMakeLists.txt index f861e8f..7ea2326 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,7 @@ cmaize_option_list( BUILD_PYBIND11_PYBINDINGS ON "Use Pybind11 to build Python bindings?" ENABLE_EXPERIMENTAL_FEATURES OFF "Build features which are not 1.0-ed yet?" ENABLE_NWCHEM ON "Should we build support for friend: NWChem ?" + ENABLE_MOLSSI ON "Build support for the MolSSI interface?" ENABLE_ASE ON "Build support for the Atomic Simulation Environment?" ) @@ -61,11 +62,12 @@ set( ## Find friends ## include(ase) +include(molssi) include(nwchem) #TOOD: Replace cmaize_add_library when it supports Python add_library(${PROJECT_NAME} INTERFACE) -target_link_libraries(${PROJECT_NAME} INTERFACE simde ase nwchem) +target_link_libraries(${PROJECT_NAME} INTERFACE simde ase molssi nwchem) if("${BUILD_TESTING}") include(CTest) diff --git a/cmake/molssi.cmake b/cmake/molssi.cmake new file mode 100644 index 0000000..e072401 --- /dev/null +++ b/cmake/molssi.cmake @@ -0,0 +1,38 @@ +# Copyright 2023 NWChemEx-Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +include_guard() + +if("${BUILD_PYBIND11_PYBINDINGS}") + include(python/python) + + #[[[ Determines if the MolSSI Python interface are installed. + # + # At present FriendZone can not install + #]] + function(find_molssi) + assert_python_module("qcelemental") + message("Found qcelemental: ${QCELEMENTAL_FOUND}") + assert_python_module("qcengine") + message("Found qcengine: ${QCENGINE_FOUND}") + assert_python_module("networkx") + message("Found networkx: ${NETWORKX_FOUND}") + endfunction() + + if("${ENABLE_MOLSSI}") + find_molssi() + endif() +endif() + +add_library(molssi INTERFACE) diff --git a/cmake/nwchem.cmake b/cmake/nwchem.cmake index c5ab929..317d813 100644 --- a/cmake/nwchem.cmake +++ b/cmake/nwchem.cmake @@ -15,17 +15,12 @@ include_guard() if("${BUILD_PYBIND11_PYBINDINGS}") - include(python/python) - #[[[ Determines if NWChem and the necessary Python interface are installed. # # At present FriendZone can not install #]] function(find_nwchem) find_program(NWCHEM_FOUND nwchem REQUIRED) - assert_python_module("qcelemental") - assert_python_module("qcengine") - assert_python_module("networkx") message("Found nwchem: ${NWCHEM_FOUND}") endfunction() diff --git a/pyproject.toml b/pyproject.toml index c00cd6d..002d3ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,7 +73,7 @@ package-dir = { "" = "src/python" } # Example: pip install friendzone[ase] [project.optional-dependencies] ase = ["ase"] -nwchem = ["qcengine", "qcelemental", "networkx"] +molssi = ["qcengine", "qcelemental", "networkx"] # Dependency groups are optional dependencies that are not intented to appear # after packaging, usually used to help with testing or development diff --git a/src/python/friendzone/__init__.py b/src/python/friendzone/__init__.py index f19a2db..352beb2 100644 --- a/src/python/friendzone/__init__.py +++ b/src/python/friendzone/__init__.py @@ -13,8 +13,7 @@ # limitations under the License. from .nwx2ase import load_ase_modules -from .nwx2qcelemental import load_qcelemental_modules -from .nwx2qcengine import load_qcengine_modules +from .nwx2molssi import load_molssi_modules def load_modules(mm): @@ -22,8 +21,7 @@ def load_modules(mm): calls the various friend specific module loading functions, including: * `load_ase_modules` - * `load_qcengine_modules` - * `load_qcelemental_modules` + * `load_molssi_modules` Note some and/or all of these may be no-ops depending on what friends were enabled. @@ -32,5 +30,4 @@ def load_modules(mm): :type mm: pluginplay.ModuleManager """ load_ase_modules(mm) - load_qcengine_modules(mm) - load_qcelemental_modules(mm) + load_molssi_modules(mm) diff --git a/src/python/friendzone/friends.py b/src/python/friendzone/friends.py index a18e694..34a036d 100644 --- a/src/python/friendzone/friends.py +++ b/src/python/friendzone/friends.py @@ -13,29 +13,37 @@ # limitations under the License. from importlib.util import find_spec +from shutil import which -def friends() -> list[str]: - """Returns a list of potentially supported friends. +def is_ase_enabled(): + """Checks whether the ASE friend is enabled by verifying that the `ase` + package is installed. - :return: A list of names of potentially supported friends. - :rtype: list[str] + :return: True if the ASE friend is enabled, False otherwise. + :rtype: bool """ - friends_list = [ - "ase", - "nwchem", - ] - return friends_list + return find_spec("ase") is not None + + +def is_molssi_enabled(): + """Checks whether the MolSSI friend is enabled by verifying that the + `qcelemental`, `qcengine`, and `networkx` packages are installed. + :return: True if the MolSSI friend is enabled, False otherwise. + :rtype: bool + """ + for req in ["qcelemental", "qcengine", "networkx"]: + if find_spec(req) is None: + return False + return True -def is_friend_enabled(friend: str) -> bool: - """Wraps the process of determining if a particular friend was enabled. - :param friend: Name of friend to check - :type friend: str +def is_nwchem_enabled(): + """Checks whether the NWChem friend is enabled by verifying that the + `nwchem` executable is available on the system PATH. - :return: True if FriendZone was configured with support for ``friend`` - and false otherwise. + :return: True if the NWChem friend is enabled, False otherwise. :rtype: bool """ - return friend in friends() and find_spec(friend) is not None + return which("nwchem") is not None diff --git a/src/python/friendzone/nwx2ase/__init__.py b/src/python/friendzone/nwx2ase/__init__.py index 7be0f96..054ead5 100644 --- a/src/python/friendzone/nwx2ase/__init__.py +++ b/src/python/friendzone/nwx2ase/__init__.py @@ -12,21 +12,23 @@ # See the License for the specific language governing permissions and # limitations under the License. -from ..friends import is_friend_enabled +from ..friends import is_ase_enabled -if is_friend_enabled("ase"): - from .nwchem_via_ase import NWChemEnergyViaASE, NWChemGradientViaASE +if is_ase_enabled(): + from .nwchem_via_ase import load_nwchem_via_ase_modules def load_ase_modules(mm): - if not is_friend_enabled("ase"): - return + """Loads the collection of all ASE modules. This function calls the various + submodule specific loading functions, including: - if is_friend_enabled("nwchem"): - for method in ["SCF", "MP2", "CCSD", "CCSD(T)"]: - egy_key = "ASE(NWChem) : " + method - grad_key = egy_key + " gradient" - mm.add_module(egy_key, NWChemEnergyViaASE()) - mm.add_module(grad_key, NWChemGradientViaASE()) - for key in [egy_key, grad_key]: - mm.change_input(key, "method", method) + * `load_nwchem_via_ase_modules` + + Note some and/or all of these may be no-ops depending on what friends were + enabled. This function is a no-op if ASE is not installed. + + :param mm: The ModuleManager that the all Modules will be loaded into. + :type mm: pluginplay.ModuleManager + """ + if is_ase_enabled(): + load_nwchem_via_ase_modules(mm) diff --git a/src/python/friendzone/nwx2ase/nwchem_via_ase.py b/src/python/friendzone/nwx2ase/nwchem_via_ase.py index 554624c..740304b 100644 --- a/src/python/friendzone/nwx2ase/nwchem_via_ase.py +++ b/src/python/friendzone/nwx2ase/nwchem_via_ase.py @@ -21,6 +21,7 @@ from ase.calculators.nwchem import NWChem from simde import EnergyNuclearGradientStdVectorD, TotalEnergy +from ..friends import is_nwchem_enabled from ..utils.unwrap_inputs import unwrap_inputs from .chemical_system_conversions import chemical_system2atoms @@ -91,3 +92,20 @@ def run_(self, inputs, submods): augrad = [-1.0 * x / (au2eV / au2ang) for x in grad] rv = TotalEnergy().wrap_results(rv, egy) return pt.wrap_results(rv, augrad) + + +def load_nwchem_via_ase_modules(mm): + """Loads the collection of all ASE(NWChem) modules. This function is a no-op + if NWChem is not installed. + + :param mm: The ModuleManager that the all Modules will be loaded into. + :type mm: pluginplay.ModuleManager + """ + if is_nwchem_enabled(): + for method in ["SCF", "MP2", "CCSD", "CCSD(T)"]: + egy_key = "ASE(NWChem) : " + method + grad_key = egy_key + " gradient" + mm.add_module(egy_key, NWChemEnergyViaASE()) + mm.add_module(grad_key, NWChemGradientViaASE()) + for key in [egy_key, grad_key]: + mm.change_input(key, "method", method) diff --git a/src/python/friendzone/nwx2molssi/__init__.py b/src/python/friendzone/nwx2molssi/__init__.py new file mode 100644 index 0000000..5c6d2a3 --- /dev/null +++ b/src/python/friendzone/nwx2molssi/__init__.py @@ -0,0 +1,42 @@ +# Copyright 2024 NWChemEx +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ..friends import is_molssi_enabled + +if is_molssi_enabled(): + from .nwchem_via_molssi import load_nwchem_via_molssi_modules + from .system_via_molssi import load_system_via_molssi_modules + + +def load_molssi_modules(mm): + """Loads the collection of all MolSSI modules. This function calls the + various submodule specific loading functions, including: + + * `load_system_via_molssi_modules` + * `load_nwchem_via_molssi_modules` + + Note some and/or all of these may be no-ops depending on what friends were + enabled. This entire function is a no-op if the following dependencies are + not installed: + + * `qcelemental` + * `qcengine` + * `networkx` + + :param mm: The ModuleManager that the all Modules will be loaded into. + :type mm: pluginplay.ModuleManager + """ + if is_molssi_enabled(): + load_system_via_molssi_modules(mm) + load_nwchem_via_molssi_modules(mm) diff --git a/src/python/friendzone/nwx2qcengine/call_qcengine.py b/src/python/friendzone/nwx2molssi/call_qcengine.py similarity index 97% rename from src/python/friendzone/nwx2qcengine/call_qcengine.py rename to src/python/friendzone/nwx2molssi/call_qcengine.py index 67e8177..a1edb39 100644 --- a/src/python/friendzone/nwx2qcengine/call_qcengine.py +++ b/src/python/friendzone/nwx2molssi/call_qcengine.py @@ -15,9 +15,7 @@ import qcelemental as qcel import qcengine as qcng -from ..nwx2qcelemental.chemical_system_conversions import ( - chemical_system2qc_mol, -) +from .chemical_system_conversions import chemical_system2qc_mol def call_qcengine(driver, mol, program, runtime, **kwargs): diff --git a/src/python/friendzone/nwx2qcelemental/chemical_system_conversions.py b/src/python/friendzone/nwx2molssi/chemical_system_conversions.py similarity index 100% rename from src/python/friendzone/nwx2qcelemental/chemical_system_conversions.py rename to src/python/friendzone/nwx2molssi/chemical_system_conversions.py diff --git a/src/python/friendzone/nwx2qcengine/__init__.py b/src/python/friendzone/nwx2molssi/nwchem_via_molssi.py similarity index 81% rename from src/python/friendzone/nwx2qcengine/__init__.py rename to src/python/friendzone/nwx2molssi/nwchem_via_molssi.py index ed9e0d3..c617754 100644 --- a/src/python/friendzone/nwx2qcengine/__init__.py +++ b/src/python/friendzone/nwx2molssi/nwchem_via_molssi.py @@ -1,4 +1,4 @@ -# Copyright 2024 NWChemEx-Project +# Copyright 2025 NWChemEx-Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,12 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. + import numpy as np import pluginplay as pp import tensorwrapper as tw from simde import EnergyNuclearGradientStdVectorD, TotalEnergy -from ..friends import is_friend_enabled +from ..friends import is_nwchem_enabled from ..utils.unwrap_inputs import unwrap_inputs from .call_qcengine import call_qcengine @@ -56,7 +57,7 @@ def _run_impl(driver, inputs, rv, runtime): return pts["energy"].wrap_results(rv, egy) -class QCEngineEnergy(pp.ModuleBase): +class _QCEngineEnergy(pp.ModuleBase): """Driver module for computing energies with QCEngine. This class relies on _run_impl to actually implement run_. @@ -65,7 +66,7 @@ class QCEngineEnergy(pp.ModuleBase): def __init__(self): pp.ModuleBase.__init__(self) self.satisfies_property_type(TotalEnergy()) - self.description(QCEngineEnergy.__doc__) + self.description(_QCEngineEnergy.__doc__) self.add_input("program").set_description("Friend to call") self.add_input("method").set_description("Level of theory") self.add_input("basis set").set_description("Name of AO basis set") @@ -74,7 +75,7 @@ def run_(self, inputs, submods): return _run_impl("energy", inputs, self.results(), self.get_runtime()) -class QCEngineGradient(QCEngineEnergy): +class _QCEngineGradient(_QCEngineEnergy): """Driver module for computing gradients with QCEngine. This class extends QCEngineEnergy (QCEngine always computes the energy @@ -86,7 +87,7 @@ class QCEngineGradient(QCEngineEnergy): """ def __init__(self): - QCEngineEnergy.__init__(self) + _QCEngineEnergy.__init__(self) self.satisfies_property_type(EnergyNuclearGradientStdVectorD()) def run_(self, inputs, submods): @@ -95,7 +96,7 @@ def run_(self, inputs, submods): ) -def load_qcengine_modules(mm): +def load_nwchem_via_molssi_modules(mm): """Loads the collection of modules that wrap QCElemental calls. Currently, the friends exported by this function are: @@ -114,18 +115,18 @@ def load_qcengine_modules(mm): The final set of modules is the Cartesian product of all of the above. + This function is a no-op if NWChem is not installed. + :param mm: The ModuleManager that the NWChem Modules will be loaded into. :type mm: pluginplay.ModuleManager """ - - for program in ["nwchem"]: - if is_friend_enabled(program): - for method in ["SCF", "B3LYP", "MP2", "CCSD", "CCSD(T)"]: - egy_key = program + " : " + method - grad_key = egy_key + " Gradient" - mm.add_module(egy_key, QCEngineEnergy()) - mm.add_module(grad_key, QCEngineGradient()) - - for key in [egy_key, grad_key]: - mm.change_input(key, "program", program) - mm.change_input(key, "method", method) + if is_nwchem_enabled(): + for method in ["SCF", "B3LYP", "MP2", "CCSD", "CCSD(T)"]: + egy_key = "nwchem" + " : " + method + grad_key = egy_key + " Gradient" + mm.add_module(egy_key, _QCEngineEnergy()) + mm.add_module(grad_key, _QCEngineGradient()) + + for key in [egy_key, grad_key]: + mm.change_input(key, "program", "nwchem") + mm.change_input(key, "method", method) diff --git a/src/python/friendzone/nwx2qcelemental/__init__.py b/src/python/friendzone/nwx2molssi/system_via_molssi.py similarity index 91% rename from src/python/friendzone/nwx2qcelemental/__init__.py rename to src/python/friendzone/nwx2molssi/system_via_molssi.py index 88a6d7e..8637b2c 100644 --- a/src/python/friendzone/nwx2qcelemental/__init__.py +++ b/src/python/friendzone/nwx2molssi/system_via_molssi.py @@ -1,4 +1,4 @@ -# Copyright 2024 NWChemEx +# Copyright 2025 NWChemEx # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import qcelemental from simde import MoleculeFromString -from ..nwx2qcelemental.chemical_system_conversions import qc_mol2molecule +from .chemical_system_conversions import qc_mol2molecule class SystemViaMolSSI(pp.ModuleBase): @@ -38,11 +38,10 @@ def run_(self, inputs, submods): return pt.wrap_results(rv, mol) -def load_qcelemental_modules(mm): +def load_system_via_molssi_modules(mm): """Loads the collection of modules that wrap QCElemental calls. :param mm: The ModuleManager that the NWChem Modules will be loaded into. :type mm: pluginplay.ModuleManager """ - mm.add_module("ChemicalSystem via QCElemental", SystemViaMolSSI()) diff --git a/tests/python/unit_tests/nwx2ase/test_chemical_system_conversions.py b/tests/python/unit_tests/nwx2ase/test_chemical_system_conversions.py index 63762c6..d9952b0 100644 --- a/tests/python/unit_tests/nwx2ase/test_chemical_system_conversions.py +++ b/tests/python/unit_tests/nwx2ase/test_chemical_system_conversions.py @@ -14,10 +14,10 @@ import unittest -from friendzone.friends import is_friend_enabled +from friendzone.friends import is_ase_enabled from molecules import make_h2, make_h2o -if is_friend_enabled("ase"): +if is_ase_enabled(): from ase import Atoms from friendzone.nwx2ase.chemical_system_conversions import ( chemical_system2atoms, @@ -68,5 +68,5 @@ def test_h2o(self): compare_ase(self, ase_mol, corr) def setUp(self): - if not is_friend_enabled("ase"): + if not is_ase_enabled(): self.skipTest("ASE is not enabled!") diff --git a/tests/python/unit_tests/nwx2ase/test_nwchem_via_ase.py b/tests/python/unit_tests/nwx2ase/test_nwchem_via_ase.py index 1b21461..5793380 100644 --- a/tests/python/unit_tests/nwx2ase/test_nwchem_via_ase.py +++ b/tests/python/unit_tests/nwx2ase/test_nwchem_via_ase.py @@ -15,7 +15,8 @@ import unittest import numpy as np -from friendzone import friends, load_modules +from friendzone import load_modules +from friendzone.friends import is_ase_enabled, is_nwchem_enabled from molecules import make_h2 from pluginplay import ModuleManager from simde import EnergyNuclearGradientStdVectorD, TotalEnergy @@ -65,10 +66,9 @@ def test_ccsd_t(self): self.assertAlmostEqual(np.array(egy), -1.122251361965036, places=4) def setUp(self): - nwchem_enabled = friends.is_friend_enabled("nwchem") - ase_enabled = friends.is_friend_enabled("ase") - - if not nwchem_enabled or not ase_enabled: + if not is_ase_enabled(): + self.skipTest("ASE is not enabled!") + elif not is_nwchem_enabled(): self.skipTest("NWChem backend is not enabled!") self.mm = ModuleManager() diff --git a/tests/python/unit_tests/nwx2qcelemental/__init__.py b/tests/python/unit_tests/nwx2molssi/__init__.py similarity index 100% rename from tests/python/unit_tests/nwx2qcelemental/__init__.py rename to tests/python/unit_tests/nwx2molssi/__init__.py diff --git a/tests/python/unit_tests/nwx2qcelemental/test_chemical_system_conversions.py b/tests/python/unit_tests/nwx2molssi/test_chemical_system_conversions.py similarity index 73% rename from tests/python/unit_tests/nwx2qcelemental/test_chemical_system_conversions.py rename to tests/python/unit_tests/nwx2molssi/test_chemical_system_conversions.py index e278979..84f3a32 100644 --- a/tests/python/unit_tests/nwx2qcelemental/test_chemical_system_conversions.py +++ b/tests/python/unit_tests/nwx2molssi/test_chemical_system_conversions.py @@ -14,14 +14,17 @@ import unittest -import qcelemental as qcel from compare_molecules import compare_molecules -from friendzone.nwx2qcelemental.chemical_system_conversions import ( - chemical_system2qc_mol, - qc_mol2molecule, -) +from friendzone.friends import is_molssi_enabled from molecules import make_h2 +if is_molssi_enabled(): + import qcelemental as qcel + from friendzone.nwx2molssi.chemical_system_conversions import ( + chemical_system2qc_mol, + qc_mol2molecule, + ) + class TestChemicalSystem2QC(unittest.TestCase): def test_h2(self): @@ -32,6 +35,10 @@ def test_h2(self): corr = qcel.models.Molecule.from_data(h2_as_str) self.assertEqual(qcel_mol, corr) + def setUp(self): + if not is_molssi_enabled(): + self.skipTest("MolSSI is not enabled!") + class TestQCMol2Molecule(unittest.TestCase): def test_h2(self): @@ -40,3 +47,7 @@ def test_h2(self): mol = qcel.models.Molecule.from_data(h2_as_str) result = qc_mol2molecule(mol) compare_molecules(self, result, corr) + + def setUp(self): + if not is_molssi_enabled(): + self.skipTest("MolSSI friend is not enabled!") diff --git a/tests/python/unit_tests/nwx2qcengine/test_nwchem.py b/tests/python/unit_tests/nwx2molssi/test_nwchem_via_molssi.py similarity index 90% rename from tests/python/unit_tests/nwx2qcengine/test_nwchem.py rename to tests/python/unit_tests/nwx2molssi/test_nwchem_via_molssi.py index 7224f4d..0665126 100644 --- a/tests/python/unit_tests/nwx2qcengine/test_nwchem.py +++ b/tests/python/unit_tests/nwx2molssi/test_nwchem_via_molssi.py @@ -15,13 +15,14 @@ import unittest import numpy as np -from friendzone import friends, load_modules +from friendzone import load_modules +from friendzone.friends import is_molssi_enabled, is_nwchem_enabled from molecules import make_h2 from pluginplay import ModuleManager from simde import EnergyNuclearGradientStdVectorD, TotalEnergy -class TestNWChem(unittest.TestCase): +class TestNWChemViaMolSSI(unittest.TestCase): def test_scf(self): mol = make_h2() key = "NWChem : SCF" @@ -65,7 +66,9 @@ def test_ccsd_t(self): self.assertAlmostEqual(np.array(egy), -1.122251361965036, places=4) def setUp(self): - if not friends.is_friend_enabled("nwchem"): + if not is_molssi_enabled(): + self.skipTest("MolSSI is not enabled!") + elif not is_nwchem_enabled(): self.skipTest("NWChem backend is not enabled!") self.mm = ModuleManager() diff --git a/tests/python/unit_tests/nwx2qcelemental/test_system_via_molssi.py b/tests/python/unit_tests/nwx2molssi/test_system_via_molssi.py similarity index 89% rename from tests/python/unit_tests/nwx2qcelemental/test_system_via_molssi.py rename to tests/python/unit_tests/nwx2molssi/test_system_via_molssi.py index c9d16ae..5e5bf23 100644 --- a/tests/python/unit_tests/nwx2qcelemental/test_system_via_molssi.py +++ b/tests/python/unit_tests/nwx2molssi/test_system_via_molssi.py @@ -16,6 +16,7 @@ from compare_molecules import compare_molecules from friendzone import load_modules +from friendzone.friends import is_molssi_enabled from molecules import make_h2 from pluginplay import ModuleManager from simde import MoleculeFromString @@ -31,6 +32,9 @@ def test_h2(self): compare_molecules(self, corr.molecule, mol) def setUp(self): + if not is_molssi_enabled(): + self.skipTest("MolSSI friend is not enabled!") + self.mm = ModuleManager() load_modules(self.mm) self.pt = MoleculeFromString() diff --git a/tests/python/unit_tests/nwx2qcengine/__init__.py b/tests/python/unit_tests/nwx2qcengine/__init__.py deleted file mode 100644 index cb2dc38..0000000 --- a/tests/python/unit_tests/nwx2qcengine/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2023 NWChemEx-Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/tests/python/unit_tests/test_friends.py b/tests/python/unit_tests/test_friends.py index a1cebf4..5b1584a 100644 --- a/tests/python/unit_tests/test_friends.py +++ b/tests/python/unit_tests/test_friends.py @@ -14,21 +14,27 @@ import unittest from importlib.util import find_spec +from shutil import which -from friendzone.friends import friends, is_friend_enabled +from friendzone import friends class TestFriends(unittest.TestCase): - def test_friends_list(self): - expected_friends = ["ase", "nwchem"] - actual_friends = friends() - self.assertCountEqual(actual_friends, expected_friends) + def test_is_ase_enabled(self): + expected = find_spec("ase") is not None + actual = friends.is_ase_enabled() + self.assertEqual(expected, actual) - def test_is_friend_enabled(self): - # For known friends, the result should match the system state - for friend in friends(): - enabled = is_friend_enabled(friend) - module_found = find_spec(friend) is not None - self.assertEqual(enabled, module_found) - # For unknown friends, the result should be False - self.assertFalse(is_friend_enabled("non_existent_friend")) + def test_is_molssi_enabled(self): + expected = True + for req in ["qcelemental", "qcengine", "networkx"]: + if find_spec(req) is None: + expected = False + break + actual = friends.is_molssi_enabled() + self.assertEqual(expected, actual) + + def test_is_nwchem_enabled(self): + expected = which("nwchem") is not None + actual = friends.is_nwchem_enabled() + self.assertEqual(expected, actual) From 90c8b73199085ceaff3b160f8bf7b91355573542 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Waldrop" Date: Tue, 11 Nov 2025 22:22:33 -0600 Subject: [PATCH 12/13] revert MolSSI module names --- .../friendzone/nwx2molssi/nwchem_via_molssi.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/python/friendzone/nwx2molssi/nwchem_via_molssi.py b/src/python/friendzone/nwx2molssi/nwchem_via_molssi.py index c617754..ecacbf4 100644 --- a/src/python/friendzone/nwx2molssi/nwchem_via_molssi.py +++ b/src/python/friendzone/nwx2molssi/nwchem_via_molssi.py @@ -57,7 +57,7 @@ def _run_impl(driver, inputs, rv, runtime): return pts["energy"].wrap_results(rv, egy) -class _QCEngineEnergy(pp.ModuleBase): +class QCEngineEnergy(pp.ModuleBase): """Driver module for computing energies with QCEngine. This class relies on _run_impl to actually implement run_. @@ -66,7 +66,7 @@ class _QCEngineEnergy(pp.ModuleBase): def __init__(self): pp.ModuleBase.__init__(self) self.satisfies_property_type(TotalEnergy()) - self.description(_QCEngineEnergy.__doc__) + self.description(QCEngineEnergy.__doc__) self.add_input("program").set_description("Friend to call") self.add_input("method").set_description("Level of theory") self.add_input("basis set").set_description("Name of AO basis set") @@ -75,7 +75,7 @@ def run_(self, inputs, submods): return _run_impl("energy", inputs, self.results(), self.get_runtime()) -class _QCEngineGradient(_QCEngineEnergy): +class QCEngineGradient(QCEngineEnergy): """Driver module for computing gradients with QCEngine. This class extends QCEngineEnergy (QCEngine always computes the energy @@ -87,7 +87,7 @@ class _QCEngineGradient(_QCEngineEnergy): """ def __init__(self): - _QCEngineEnergy.__init__(self) + QCEngineEnergy.__init__(self) self.satisfies_property_type(EnergyNuclearGradientStdVectorD()) def run_(self, inputs, submods): @@ -124,8 +124,8 @@ def load_nwchem_via_molssi_modules(mm): for method in ["SCF", "B3LYP", "MP2", "CCSD", "CCSD(T)"]: egy_key = "nwchem" + " : " + method grad_key = egy_key + " Gradient" - mm.add_module(egy_key, _QCEngineEnergy()) - mm.add_module(grad_key, _QCEngineGradient()) + mm.add_module(egy_key, QCEngineEnergy()) + mm.add_module(grad_key, QCEngineGradient()) for key in [egy_key, grad_key]: mm.change_input(key, "program", "nwchem") From 61cd73a0a0079f28f8642111af62acbadfc4cb63 Mon Sep 17 00:00:00 2001 From: "Jonathan M. Waldrop" Date: Thu, 13 Nov 2025 15:13:40 -0600 Subject: [PATCH 13/13] CMake updates and Zach's suggestions --- .licenserc.yaml | 1 + CMakeLists.txt | 2 +- cmake/ase.cmake | 4 +- cmake/molssi.cmake | 8 ++- cmake/nwchem.cmake | 4 +- pyproject.toml | 4 +- src/python/friendzone/__init__.py | 16 +++--- src/python/friendzone/friends.py | 9 ++-- src/python/friendzone/nwx2ase/__init__.py | 14 ++++-- .../friendzone/nwx2ase/nwchem_via_ase.py | 10 +++- src/python/friendzone/nwx2molssi/__init__.py | 24 +++++---- .../nwx2molssi/nwchem_via_molssi.py | 4 +- .../nwx2molssi/system_via_molssi.py | 4 +- tests/python/unit_tests/test_friends.py | 50 +++++++++++++------ 14 files changed, 91 insertions(+), 63 deletions(-) diff --git a/.licenserc.yaml b/.licenserc.yaml index ea83d21..713404b 100644 --- a/.licenserc.yaml +++ b/.licenserc.yaml @@ -20,6 +20,7 @@ header: paths-ignore: - .github/ - docs/Makefile + - docs/build/ - LICENSE - docs/requirements.txt - docs/source/bibliography/*.bib diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ea2326..e7ddbec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -67,7 +67,7 @@ include(nwchem) #TOOD: Replace cmaize_add_library when it supports Python add_library(${PROJECT_NAME} INTERFACE) -target_link_libraries(${PROJECT_NAME} INTERFACE simde ase molssi nwchem) +target_link_libraries(${PROJECT_NAME} INTERFACE simde) if("${BUILD_TESTING}") include(CTest) diff --git a/cmake/ase.cmake b/cmake/ase.cmake index c8ced54..99d97ab 100644 --- a/cmake/ase.cmake +++ b/cmake/ase.cmake @@ -23,12 +23,10 @@ if("${BUILD_PYBIND11_PYBINDINGS}") #]] function(find_ase) assert_python_module("ase") - message("Found ASE: ${ASE_FOUND}") + message(STATUS "Found ASE: ${ASE_FOUND}") endfunction() if("${ENABLE_ASE}") find_ase() endif() endif() - -add_library(ase INTERFACE) diff --git a/cmake/molssi.cmake b/cmake/molssi.cmake index e072401..3341ee5 100644 --- a/cmake/molssi.cmake +++ b/cmake/molssi.cmake @@ -23,16 +23,14 @@ if("${BUILD_PYBIND11_PYBINDINGS}") #]] function(find_molssi) assert_python_module("qcelemental") - message("Found qcelemental: ${QCELEMENTAL_FOUND}") + message(STATUS "Found qcelemental: ${QCELEMENTAL_FOUND}") assert_python_module("qcengine") - message("Found qcengine: ${QCENGINE_FOUND}") + message(STATUS "Found qcengine: ${QCENGINE_FOUND}") assert_python_module("networkx") - message("Found networkx: ${NETWORKX_FOUND}") + message(STATUS "Found networkx: ${NETWORKX_FOUND}") endfunction() if("${ENABLE_MOLSSI}") find_molssi() endif() endif() - -add_library(molssi INTERFACE) diff --git a/cmake/nwchem.cmake b/cmake/nwchem.cmake index 317d813..26f1879 100644 --- a/cmake/nwchem.cmake +++ b/cmake/nwchem.cmake @@ -21,12 +21,10 @@ if("${BUILD_PYBIND11_PYBINDINGS}") #]] function(find_nwchem) find_program(NWCHEM_FOUND nwchem REQUIRED) - message("Found nwchem: ${NWCHEM_FOUND}") + message(STATUS "Found nwchem: ${NWCHEM_FOUND}") endfunction() if("${ENABLE_NWCHEM}") find_nwchem() endif() endif() - -add_library(nwchem INTERFACE) diff --git a/pyproject.toml b/pyproject.toml index 002d3ea..7e50ebe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,10 +77,10 @@ molssi = ["qcengine", "qcelemental", "networkx"] # Dependency groups are optional dependencies that are not intented to appear # after packaging, usually used to help with testing or development -# Example: pip install . --group dev +# Example: pip install --group dev [dependency-groups] test = ["pytest"] -dev = [{ include-group = "test" }] +dev = [{ include-group = "test" }, "tox", "pre-commit"] [tool.pytest.ini_options] minversion = "8.0" diff --git a/src/python/friendzone/__init__.py b/src/python/friendzone/__init__.py index 352beb2..445b43e 100644 --- a/src/python/friendzone/__init__.py +++ b/src/python/friendzone/__init__.py @@ -17,14 +17,18 @@ def load_modules(mm): - """Loads the collection of all modules provided by Friendzone. This function - calls the various friend specific module loading functions, including: + """Loads the collection of all modules provided by Friendzone. - * `load_ase_modules` - * `load_molssi_modules` + This function calls the various friend specific module loading functions, + including: - Note some and/or all of these may be no-ops depending on what friends were - enabled. + * ``load_ase_modules`` + * ``load_molssi_modules`` + + .. note:: + + Some and/or all of these may be no-ops depending on what friends were + enabled. :param mm: The ModuleManager that the all Modules will be loaded into. :type mm: pluginplay.ModuleManager diff --git a/src/python/friendzone/friends.py b/src/python/friendzone/friends.py index 34a036d..c623247 100644 --- a/src/python/friendzone/friends.py +++ b/src/python/friendzone/friends.py @@ -17,8 +17,7 @@ def is_ase_enabled(): - """Checks whether the ASE friend is enabled by verifying that the `ase` - package is installed. + """Checks whether the ASE friend is enabled. :return: True if the ASE friend is enabled, False otherwise. :rtype: bool @@ -27,8 +26,7 @@ def is_ase_enabled(): def is_molssi_enabled(): - """Checks whether the MolSSI friend is enabled by verifying that the - `qcelemental`, `qcengine`, and `networkx` packages are installed. + """Checks whether the MolSSI friend is enabled. :return: True if the MolSSI friend is enabled, False otherwise. :rtype: bool @@ -40,8 +38,7 @@ def is_molssi_enabled(): def is_nwchem_enabled(): - """Checks whether the NWChem friend is enabled by verifying that the - `nwchem` executable is available on the system PATH. + """Checks whether the NWChem friend is enabled. :return: True if the NWChem friend is enabled, False otherwise. :rtype: bool diff --git a/src/python/friendzone/nwx2ase/__init__.py b/src/python/friendzone/nwx2ase/__init__.py index 054ead5..e56e675 100644 --- a/src/python/friendzone/nwx2ase/__init__.py +++ b/src/python/friendzone/nwx2ase/__init__.py @@ -19,13 +19,17 @@ def load_ase_modules(mm): - """Loads the collection of all ASE modules. This function calls the various - submodule specific loading functions, including: + """Loads the collection of all ASE modules. - * `load_nwchem_via_ase_modules` + This function calls the various submodule specific loading functions, + including: - Note some and/or all of these may be no-ops depending on what friends were - enabled. This function is a no-op if ASE is not installed. + * ``load_nwchem_via_ase_modules`` + + .. note:: + + Some and/or all of these may be no-ops depending on what friends were + enabled. This function is a no-op if ASE is not installed. :param mm: The ModuleManager that the all Modules will be loaded into. :type mm: pluginplay.ModuleManager diff --git a/src/python/friendzone/nwx2ase/nwchem_via_ase.py b/src/python/friendzone/nwx2ase/nwchem_via_ase.py index 740304b..fb72c27 100644 --- a/src/python/friendzone/nwx2ase/nwchem_via_ase.py +++ b/src/python/friendzone/nwx2ase/nwchem_via_ase.py @@ -95,17 +95,23 @@ def run_(self, inputs, submods): def load_nwchem_via_ase_modules(mm): - """Loads the collection of all ASE(NWChem) modules. This function is a no-op - if NWChem is not installed. + """Loads the collection of all ASE(NWChem) modules. + + .. note:: + + This function is a no-op if NWChem is not installed. :param mm: The ModuleManager that the all Modules will be loaded into. :type mm: pluginplay.ModuleManager """ if is_nwchem_enabled(): + # Loop over methods and add energy and gradient modules for each for method in ["SCF", "MP2", "CCSD", "CCSD(T)"]: egy_key = "ASE(NWChem) : " + method grad_key = egy_key + " gradient" + mm.add_module(egy_key, NWChemEnergyViaASE()) mm.add_module(grad_key, NWChemGradientViaASE()) + for key in [egy_key, grad_key]: mm.change_input(key, "method", method) diff --git a/src/python/friendzone/nwx2molssi/__init__.py b/src/python/friendzone/nwx2molssi/__init__.py index 5c6d2a3..8d5c3af 100644 --- a/src/python/friendzone/nwx2molssi/__init__.py +++ b/src/python/friendzone/nwx2molssi/__init__.py @@ -20,19 +20,23 @@ def load_molssi_modules(mm): - """Loads the collection of all MolSSI modules. This function calls the - various submodule specific loading functions, including: + """Loads the collection of all MolSSI modules. - * `load_system_via_molssi_modules` - * `load_nwchem_via_molssi_modules` + This function calls the various submodule specific loading functions, + including: - Note some and/or all of these may be no-ops depending on what friends were - enabled. This entire function is a no-op if the following dependencies are - not installed: + * ``load_system_via_molssi_modules`` + * ``load_nwchem_via_molssi_modules`` - * `qcelemental` - * `qcengine` - * `networkx` + .. note:: + + Some and/or all of these may be no-ops depending on what friends were + enabled. This entire function is a no-op if the following dependencies + are not installed: + + * ``qcelemental`` + * ``qcengine`` + * ``networkx`` :param mm: The ModuleManager that the all Modules will be loaded into. :type mm: pluginplay.ModuleManager diff --git a/src/python/friendzone/nwx2molssi/nwchem_via_molssi.py b/src/python/friendzone/nwx2molssi/nwchem_via_molssi.py index ecacbf4..124b250 100644 --- a/src/python/friendzone/nwx2molssi/nwchem_via_molssi.py +++ b/src/python/friendzone/nwx2molssi/nwchem_via_molssi.py @@ -115,7 +115,9 @@ def load_nwchem_via_molssi_modules(mm): The final set of modules is the Cartesian product of all of the above. - This function is a no-op if NWChem is not installed. + .. note:: + + This function is a no-op if NWChem is not installed. :param mm: The ModuleManager that the NWChem Modules will be loaded into. :type mm: pluginplay.ModuleManager diff --git a/src/python/friendzone/nwx2molssi/system_via_molssi.py b/src/python/friendzone/nwx2molssi/system_via_molssi.py index 8637b2c..93a5431 100644 --- a/src/python/friendzone/nwx2molssi/system_via_molssi.py +++ b/src/python/friendzone/nwx2molssi/system_via_molssi.py @@ -20,9 +20,7 @@ class SystemViaMolSSI(pp.ModuleBase): - """Creates an NWChemEx ChemicalSystem by going through MolSSI's string - parser. - """ + """Creates an NWChemEx ChemicalSystem by using MolSSI's string parser.""" def __init__(self): pp.ModuleBase.__init__(self) diff --git a/tests/python/unit_tests/test_friends.py b/tests/python/unit_tests/test_friends.py index 5b1584a..83f0370 100644 --- a/tests/python/unit_tests/test_friends.py +++ b/tests/python/unit_tests/test_friends.py @@ -13,28 +13,46 @@ # limitations under the License. import unittest -from importlib.util import find_spec -from shutil import which +from unittest.mock import patch from friendzone import friends +@patch("friendzone.friends.find_spec") +@patch("friendzone.friends.which") class TestFriends(unittest.TestCase): - def test_is_ase_enabled(self): - expected = find_spec("ase") is not None + def test_is_ase_enabled_when_detected(self, mock_which, mock_find_spec): + mock_find_spec.return_value = True actual = friends.is_ase_enabled() - self.assertEqual(expected, actual) - - def test_is_molssi_enabled(self): - expected = True - for req in ["qcelemental", "qcengine", "networkx"]: - if find_spec(req) is None: - expected = False - break + self.assertTrue(actual) + + def test_is_ase_enabled_when_not_detected( + self, mock_which, mock_find_spec + ): + mock_find_spec.return_value = None + actual = friends.is_ase_enabled() + self.assertFalse(actual) + + def test_is_molssi_enabled_when_detected(self, mock_which, mock_find_spec): + mock_find_spec.return_value = True actual = friends.is_molssi_enabled() - self.assertEqual(expected, actual) + self.assertTrue(actual) + + def test_is_molssi_enabled_when_not_detected( + self, mock_which, mock_find_spec + ): + mock_find_spec.return_value = None + actual = friends.is_molssi_enabled() + self.assertFalse(actual) + + def test_is_nwchem_enabled_when_detected(self, mock_which, mock_find_spec): + mock_which.return_value = True + actual = friends.is_nwchem_enabled() + self.assertTrue(actual) - def test_is_nwchem_enabled(self): - expected = which("nwchem") is not None + def test_is_nwchem_enabled_when_not_detected( + self, mock_which, mock_find_spec + ): + mock_which.return_value = None actual = friends.is_nwchem_enabled() - self.assertEqual(expected, actual) + self.assertFalse(actual)