diff --git a/.github/workflows/merge.yaml b/.github/workflows/merge.yaml index ab773d2..6a6f30e 100644 --- a/.github/workflows/merge.yaml +++ b/.github/workflows/merge.yaml @@ -41,6 +41,15 @@ jobs: permissions: id-token: write steps: + # build_pypi_dist imports the built wheel to check it, and importing + # friendzone imports pluginplay, whose prebuilt wheel links against the + # system libmpi. build_pypi_dist only installs MPI for compiled + # (cibuildwheel) packages, and bare GitHub runners have none, so install + # the same runtime MPI it uses there. + - name: Install Runtime MPI + run: | + sudo apt-get update + sudo apt-get install -y libopenmpi-dev openmpi-bin - name: Build PyPI Distribution uses: NWChemEx/.github/.github/actions/build_pypi_dist@master diff --git a/.github/workflows/test_python_build.yaml b/.github/workflows/test_python_build.yaml index 72a4ba1..9e65d12 100644 --- a/.github/workflows/test_python_build.yaml +++ b/.github/workflows/test_python_build.yaml @@ -38,6 +38,15 @@ jobs: test_pypi_deploy: runs-on: ubuntu-latest steps: + # build_pypi_dist imports the built wheel to check it, and importing + # friendzone imports pluginplay, whose prebuilt wheel links against the + # system libmpi. build_pypi_dist only installs MPI for compiled + # (cibuildwheel) packages, and bare GitHub runners have none, so install + # the same runtime MPI it uses there. + - name: Install Runtime MPI + run: | + sudo apt-get update + sudo apt-get install -y libopenmpi-dev openmpi-bin - name: Build PyPI Distribution uses: NWChemEx/.github/.github/actions/build_pypi_dist@master diff --git a/docs/source/nitpick_exceptions b/docs/source/nitpick_exceptions index e74883f..3071f93 100644 --- a/docs/source/nitpick_exceptions +++ b/docs/source/nitpick_exceptions @@ -1,4 +1,6 @@ py:class pluginplay.ModuleManager +py:class pluginplay.Plugin +py:obj pluginplay.Plugin py:class pluginplay.PropertyType py:class chemist.ChemicalSystem diff --git a/pyproject.toml b/pyproject.toml index 3fafe29..b1993d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] dynamic = ["version"] -dependencies = ["nwchemex-simde>=0.0.77"] +dependencies = ["nwchemex-simde>=0.0.85"] [project.optional-dependencies] ase = ["ase"] diff --git a/python/friendzone/__init__.py b/python/friendzone/__init__.py index 445b43e..772ff71 100644 --- a/python/friendzone/__init__.py +++ b/python/friendzone/__init__.py @@ -12,15 +12,19 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pluginplay + from .nwx2ase import load_ase_modules from .nwx2molssi import load_molssi_modules -def load_modules(mm): - """Loads the collection of all modules provided by Friendzone. +class FriendZone(pluginplay.Plugin): + """The FriendZone plugin. - This function calls the various friend specific module loading functions, - including: + Load it into a ModuleManager with + ``mm.load_plugin(friendzone.FriendZone())``. The plugin's name is + "friendzone". Its modules are added by the various friend specific module + loading functions, including: * ``load_ase_modules`` * ``load_molssi_modules`` @@ -29,9 +33,10 @@ def load_modules(mm): 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 """ - load_ase_modules(mm) - load_molssi_modules(mm) + + def __init__(self): + pluginplay.Plugin.__init__(self) + self.set_name("friendzone") + load_ase_modules(self) + load_molssi_modules(self) diff --git a/python/friendzone/nwx2ase/__init__.py b/python/friendzone/nwx2ase/__init__.py index e56e675..b2aa344 100644 --- a/python/friendzone/nwx2ase/__init__.py +++ b/python/friendzone/nwx2ase/__init__.py @@ -18,8 +18,8 @@ from .nwchem_via_ase import load_nwchem_via_ase_modules -def load_ase_modules(mm): - """Loads the collection of all ASE modules. +def load_ase_modules(plugin): + """Adds the collection of all ASE modules to a plugin. This function calls the various submodule specific loading functions, including: @@ -31,8 +31,8 @@ def load_ase_modules(mm): 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 + :param plugin: The plugin the modules will be added to. + :type plugin: pluginplay.Plugin """ if is_ase_enabled(): - load_nwchem_via_ase_modules(mm) + load_nwchem_via_ase_modules(plugin) diff --git a/python/friendzone/nwx2ase/nwchem_via_ase.py b/python/friendzone/nwx2ase/nwchem_via_ase.py index fb72c27..1b9e764 100644 --- a/python/friendzone/nwx2ase/nwchem_via_ase.py +++ b/python/friendzone/nwx2ase/nwchem_via_ase.py @@ -94,15 +94,15 @@ def run_(self, inputs, submods): return pt.wrap_results(rv, augrad) -def load_nwchem_via_ase_modules(mm): - """Loads the collection of all ASE(NWChem) modules. +def load_nwchem_via_ase_modules(plugin): + """Adds the collection of all ASE(NWChem) modules to a plugin. .. 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 + :param plugin: The plugin the modules will be added to. + :type plugin: pluginplay.Plugin """ if is_nwchem_enabled(): # Loop over methods and add energy and gradient modules for each @@ -110,8 +110,8 @@ def load_nwchem_via_ase_modules(mm): egy_key = "ASE(NWChem) : " + method grad_key = egy_key + " gradient" - mm.add_module(egy_key, NWChemEnergyViaASE()) - mm.add_module(grad_key, NWChemGradientViaASE()) + plugin.add_module(egy_key, NWChemEnergyViaASE()) + plugin.add_module(grad_key, NWChemGradientViaASE()) for key in [egy_key, grad_key]: - mm.change_input(key, "method", method) + plugin.modules().change_input(key, "method", method) diff --git a/python/friendzone/nwx2molssi/__init__.py b/python/friendzone/nwx2molssi/__init__.py index f2035cb..393fbd6 100644 --- a/python/friendzone/nwx2molssi/__init__.py +++ b/python/friendzone/nwx2molssi/__init__.py @@ -20,8 +20,8 @@ from .system_via_molssi import load_system_via_molssi_modules -def load_molssi_modules(mm): - """Loads the collection of all MolSSI modules. +def load_molssi_modules(plugin): + """Adds the collection of all MolSSI modules to a plugin. This function calls the various submodule specific loading functions, including: @@ -40,10 +40,10 @@ def load_molssi_modules(mm): * ``qcengine`` * ``networkx`` - :param mm: The ModuleManager that the all Modules will be loaded into. - :type mm: pluginplay.ModuleManager + :param plugin: The plugin the modules will be added to. + :type plugin: pluginplay.Plugin """ if is_molssi_enabled(): - load_system_via_molssi_modules(mm) - load_nwchem_via_molssi_modules(mm) - load_qcschema_driver_modules(mm) + load_system_via_molssi_modules(plugin) + load_nwchem_via_molssi_modules(plugin) + load_qcschema_driver_modules(plugin) diff --git a/python/friendzone/nwx2molssi/nwchem_via_molssi.py b/python/friendzone/nwx2molssi/nwchem_via_molssi.py index ef82619..5fb254a 100644 --- a/python/friendzone/nwx2molssi/nwchem_via_molssi.py +++ b/python/friendzone/nwx2molssi/nwchem_via_molssi.py @@ -96,8 +96,8 @@ def run_(self, inputs, submods): ) -def load_nwchem_via_molssi_modules(mm): - """Loads the collection of modules that wrap QCElemental calls. +def load_nwchem_via_molssi_modules(plugin): + """Adds the collection of modules that wrap QCEngine calls to a plugin. Currently, the friends exported by this function are: @@ -119,16 +119,16 @@ def load_nwchem_via_molssi_modules(mm): 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 + :param plugin: The plugin the NWChem modules will be added to. + :type plugin: pluginplay.Plugin """ 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()) + plugin.add_module(egy_key, QCEngineEnergy()) + plugin.add_module(grad_key, QCEngineGradient()) for key in [egy_key, grad_key]: - mm.change_input(key, "program", "nwchem") - mm.change_input(key, "method", method) + plugin.modules().change_input(key, "program", "nwchem") + plugin.modules().change_input(key, "method", method) diff --git a/python/friendzone/nwx2molssi/qcschema_driver.py b/python/friendzone/nwx2molssi/qcschema_driver.py index 5cec6e3..712fd18 100644 --- a/python/friendzone/nwx2molssi/qcschema_driver.py +++ b/python/friendzone/nwx2molssi/qcschema_driver.py @@ -108,11 +108,10 @@ def run_(self, inputs, submods): return pt.wrap_results(rv, result) -def load_qcschema_driver_modules(mm): - """Loads the QCSchemaDriver module. +def load_qcschema_driver_modules(plugin): + """Adds the QCSchemaDriver module to a plugin. - :param mm: The ModuleManager that the QCSchemaDriver module will be - loaded into. - :type mm: pluginplay.ModuleManager + :param plugin: The plugin the QCSchemaDriver module will be added to. + :type plugin: pluginplay.Plugin """ - mm.add_module("QCSchema Driver", QCSchemaDriver()) + plugin.add_module("QCSchema Driver", QCSchemaDriver()) diff --git a/python/friendzone/nwx2molssi/system_via_molssi.py b/python/friendzone/nwx2molssi/system_via_molssi.py index 2fab336..fde5ee0 100644 --- a/python/friendzone/nwx2molssi/system_via_molssi.py +++ b/python/friendzone/nwx2molssi/system_via_molssi.py @@ -38,10 +38,10 @@ def run_(self, inputs, submods): return pt.wrap_results(rv, mol) -def load_system_via_molssi_modules(mm): - """Loads the collection of modules that wrap QCElemental calls. +def load_system_via_molssi_modules(plugin): + """Adds the collection of modules that wrap QCElemental calls to a plugin. - :param mm: The ModuleManager that the NWChem Modules will be loaded into. - :type mm: pluginplay.ModuleManager + :param plugin: The plugin the modules will be added to. + :type plugin: pluginplay.Plugin """ - mm.add_module("ChemicalSystem via QCElemental", SystemViaMolSSI()) + plugin.add_module("ChemicalSystem via QCElemental", SystemViaMolSSI()) 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 5793380..70b2d68 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,7 @@ import unittest import numpy as np -from friendzone import load_modules +from friendzone import FriendZone from friendzone.friends import is_ase_enabled, is_nwchem_enabled from molecules import make_h2 from pluginplay import ModuleManager @@ -72,4 +72,4 @@ def setUp(self): self.skipTest("NWChem backend is not enabled!") self.mm = ModuleManager() - load_modules(self.mm) + self.mm.load_plugin(FriendZone()) diff --git a/tests/python/unit_tests/nwx2molssi/test_nwchem_via_molssi.py b/tests/python/unit_tests/nwx2molssi/test_nwchem_via_molssi.py index 0665126..5e21a27 100644 --- a/tests/python/unit_tests/nwx2molssi/test_nwchem_via_molssi.py +++ b/tests/python/unit_tests/nwx2molssi/test_nwchem_via_molssi.py @@ -15,7 +15,7 @@ import unittest import numpy as np -from friendzone import load_modules +from friendzone import FriendZone from friendzone.friends import is_molssi_enabled, is_nwchem_enabled from molecules import make_h2 from pluginplay import ModuleManager @@ -72,4 +72,4 @@ def setUp(self): self.skipTest("NWChem backend is not enabled!") self.mm = ModuleManager() - load_modules(self.mm) + self.mm.load_plugin(FriendZone()) diff --git a/tests/python/unit_tests/nwx2molssi/test_qcschema_driver.py b/tests/python/unit_tests/nwx2molssi/test_qcschema_driver.py index d4d682f..6d6a010 100644 --- a/tests/python/unit_tests/nwx2molssi/test_qcschema_driver.py +++ b/tests/python/unit_tests/nwx2molssi/test_qcschema_driver.py @@ -14,7 +14,7 @@ import unittest -from friendzone import load_modules +from friendzone import FriendZone from friendzone.friends import is_molssi_enabled if is_molssi_enabled(): @@ -101,7 +101,7 @@ def setUp(self): self.skipTest("MolSSI friend is not enabled!") self.mm = ModuleManager() - load_modules(self.mm) + self.mm.load_plugin(FriendZone()) self.mm.add_module("Dummy AOEnergy", DummyAOEnergyModule()) self.mm.change_submod("QCSchema Driver", "AOEnergy", "Dummy AOEnergy") diff --git a/tests/python/unit_tests/nwx2molssi/test_system_via_molssi.py b/tests/python/unit_tests/nwx2molssi/test_system_via_molssi.py index 5e5bf23..c64652d 100644 --- a/tests/python/unit_tests/nwx2molssi/test_system_via_molssi.py +++ b/tests/python/unit_tests/nwx2molssi/test_system_via_molssi.py @@ -15,7 +15,7 @@ import unittest from compare_molecules import compare_molecules -from friendzone import load_modules +from friendzone import FriendZone from friendzone.friends import is_molssi_enabled from molecules import make_h2 from pluginplay import ModuleManager @@ -36,5 +36,5 @@ def setUp(self): self.skipTest("MolSSI friend is not enabled!") self.mm = ModuleManager() - load_modules(self.mm) + self.mm.load_plugin(FriendZone()) self.pt = MoleculeFromString() diff --git a/tests/python/unit_tests/test_plugin.py b/tests/python/unit_tests/test_plugin.py new file mode 100644 index 0000000..b3487ae --- /dev/null +++ b/tests/python/unit_tests/test_plugin.py @@ -0,0 +1,41 @@ +# Copyright 2026 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 friendzone import FriendZone, friends +from pluginplay import ModuleManager + + +class TestFriendZonePlugin(unittest.TestCase): + def test_identity(self): + self.assertEqual(self.plugin.name(), "friendzone") + self.assertEqual(self.plugin.preferred_providers(), []) + + def test_modules(self): + modules = self.plugin.get_modules() + has_molssi = friends.is_molssi_enabled() + self.assertEqual(modules.count("QCSchema Driver"), int(has_molssi)) + self.assertEqual( + modules.count("ChemicalSystem via QCElemental"), int(has_molssi) + ) + + def test_load_plugin(self): + mm = ModuleManager() + mm.load_plugin(FriendZone()) + self.assertTrue(mm.has_plugin("friendzone")) + self.assertEqual(mm.size(), self.plugin.get_modules().size()) + + def setUp(self): + self.plugin = FriendZone()