diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ca34f397..f23db3dd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,7 +33,16 @@ jobs: python -m black . --diff --check --verbose python -m isort . --diff --check-only --verbose python -m flake8 momentGW/ --verbose - - name: Run unit tests + #- name: Run unit tests + # run: | + # python -m pip install pytest pytest-cov + # pytest --cov momentGW/ + - name: Build documentation run: | - python -m pip install pytest pytest-cov - pytest --cov momentGW/ + cd docs + make html + - name: Deploy documentation + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs/_build/html diff --git a/README.md b/README.md index c3414a1f..8f8fdc71 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,11 @@ The `examples` directory contains more detailed usage examples. ### Publications The methods implemented in this package have been described in the following papers: -- [*"A 'moment-conserving' reformulation of GW theory"*](https://doi.org/10.1063/5.0143291) +- [A 'moment-conserving' reformulation of GW theory](https://doi.org/10.1063/5.0143291), *J. Chem. Phys.* 158, 124102 (2023). The data presented in the publications can be found in the `benchmark` directory. ### Contributing Contributions are welcome, and can be made by submitting a pull request to the `master` branch. -The code uses [NumPy-style docstrings](https://numpydoc.readthedocs.io/en/latest/format.html) and is formatted using [`black`](https://black.readthedocs.io/en/stable/), [`isort`](https://pycqa.github.io/isort/), [`ssort`](https://github.com/bwhmather/ssort), and [`flake8`](https://flake8.pycqa.org/en/latest/). +The code uses [NumPy-style docstrings](https://numpydoc.readthedocs.io/en/latest/format.html) and is formatted using [black](https://black.readthedocs.io/en/stable/), [isort](https://pycqa.github.io/isort/), [ssort](https://github.com/bwhmather/ssort), and [flake8](https://flake8.pycqa.org/en/latest/). diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..85158a06 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,21 @@ +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +html: + sphinx-build -M html . _build + +postprocess: + # Post-processing generated files: + # If there is a line with a "Duplicate implicit target name" error, remove it: + for file in `find _build/html -name "*.html"`; do \ + sed -i '/Duplicate implicit target name/d' $$file; \ + done + +docs: html postprocess + +clean: + rm -rf _build autoapi + +.PHONY: html postprocess clean diff --git a/docs/_templates/autoapi/base/base.rst b/docs/_templates/autoapi/base/base.rst new file mode 100644 index 00000000..45c8dd7f --- /dev/null +++ b/docs/_templates/autoapi/base/base.rst @@ -0,0 +1,7 @@ +.. {{ obj.type }}:: {{ obj.name }} + + {% if summary %} + + {{ obj.summary }} + + {% endif %} diff --git a/docs/_templates/autoapi/index.rst b/docs/_templates/autoapi/index.rst new file mode 100644 index 00000000..95d0ad89 --- /dev/null +++ b/docs/_templates/autoapi/index.rst @@ -0,0 +1,15 @@ +API Reference +============= + +This page contains auto-generated API reference documentation [#f1]_. + +.. toctree:: + :titlesonly: + + {% for page in pages %} + {% if page.top_level_object and page.display %} + {{ page.include_path }} + {% endif %} + {% endfor %} + +.. [#f1] Created with `sphinx-autoapi `_ diff --git a/docs/_templates/autoapi/python/attribute.rst b/docs/_templates/autoapi/python/attribute.rst new file mode 100644 index 00000000..ebaba555 --- /dev/null +++ b/docs/_templates/autoapi/python/attribute.rst @@ -0,0 +1 @@ +{% extends "python/data.rst" %} diff --git a/docs/_templates/autoapi/python/class.rst b/docs/_templates/autoapi/python/class.rst new file mode 100644 index 00000000..a584dcd6 --- /dev/null +++ b/docs/_templates/autoapi/python/class.rst @@ -0,0 +1,60 @@ +{% if obj.display %} +.. py:{{ obj.type }}:: {{ obj.short_name }}{% if obj.args %}({{ obj.args }}){% endif %} + +{% for (args, return_annotation) in obj.overloads %} + {{ " " * (obj.type | length) }} {{ obj.short_name }}{% if args %}({{ args }}){% endif %} + +{% endfor %} + + + {% if obj.bases %} + {% if "show-inheritance" in autoapi_options %} + Bases: {% for base in obj.bases %}{{ base|link_objs }}{% if not loop.last %}, {% endif %}{% endfor %} + {% endif %} + + + {% if "show-inheritance-diagram" in autoapi_options and obj.bases != ["object"] %} + .. autoapi-inheritance-diagram:: {{ obj.obj["full_name"] }} + :parts: 1 + {% if "private-members" in autoapi_options %} + :private-bases: + {% endif %} + + {% endif %} + {% endif %} + {% if obj.docstring %} + {{ obj.docstring|indent(3) }} + {% endif %} + {% if "inherited-members" in autoapi_options %} + {% set visible_classes = obj.classes|selectattr("display")|list %} + {% else %} + {% set visible_classes = obj.classes|rejectattr("inherited")|selectattr("display")|list %} + {% endif %} + {% for klass in visible_classes %} + {{ klass.render()|indent(3) }} + {% endfor %} + {% if "inherited-members" in autoapi_options %} + {% set visible_properties = obj.properties|selectattr("display")|list %} + {% else %} + {% set visible_properties = obj.properties|rejectattr("inherited")|selectattr("display")|list %} + {% endif %} + {% for property in visible_properties %} + {{ property.render()|indent(3) }} + {% endfor %} + {% if "inherited-members" in autoapi_options %} + {% set visible_attributes = obj.attributes|selectattr("display")|list %} + {% else %} + {% set visible_attributes = obj.attributes|rejectattr("inherited")|selectattr("display")|list %} + {% endif %} + {% for attribute in visible_attributes %} + {{ attribute.render()|indent(3) }} + {% endfor %} + {% if "inherited-members" in autoapi_options %} + {% set visible_methods = obj.methods|selectattr("display")|list %} + {% else %} + {% set visible_methods = obj.methods|rejectattr("inherited")|selectattr("display")|list %} + {% endif %} + {% for method in visible_methods %} + {{ method.render()|indent(3) }} + {% endfor %} +{% endif %} diff --git a/docs/_templates/autoapi/python/data.rst b/docs/_templates/autoapi/python/data.rst new file mode 100644 index 00000000..3d12b2d0 --- /dev/null +++ b/docs/_templates/autoapi/python/data.rst @@ -0,0 +1,37 @@ +{% if obj.display %} +.. py:{{ obj.type }}:: {{ obj.name }} + {%- if obj.annotation is not none %} + + :type: {%- if obj.annotation %} {{ obj.annotation }}{%- endif %} + + {%- endif %} + + {%- if obj.value is not none %} + + :value: {% if obj.value is string and obj.value.splitlines()|count > 1 -%} + Multiline-String + + .. raw:: html + +
Show Value + + .. code-block:: python + + """{{ obj.value|indent(width=8,blank=true) }}""" + + .. raw:: html + +
+ + {%- else -%} + {%- if obj.value is string -%} + {{ "%r" % obj.value|string|truncate(100) }} + {%- else -%} + {{ obj.value|string|truncate(100) }} + {%- endif -%} + {%- endif %} + {%- endif %} + + + {{ obj.docstring|indent(3) }} +{% endif %} diff --git a/docs/_templates/autoapi/python/exception.rst b/docs/_templates/autoapi/python/exception.rst new file mode 100644 index 00000000..92f3d38f --- /dev/null +++ b/docs/_templates/autoapi/python/exception.rst @@ -0,0 +1 @@ +{% extends "python/class.rst" %} diff --git a/docs/_templates/autoapi/python/function.rst b/docs/_templates/autoapi/python/function.rst new file mode 100644 index 00000000..778ba9a8 --- /dev/null +++ b/docs/_templates/autoapi/python/function.rst @@ -0,0 +1,15 @@ +{% if obj.display %} +.. py:function:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %} + +{% for (args, return_annotation) in obj.overloads %} + {{ obj.short_name }}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %} + +{% endfor %} + {% for property in obj.properties %} + :{{ property }}: + {% endfor %} + + {% if obj.docstring %} + {{ obj.docstring|indent(3) }} + {% endif %} +{% endif %} diff --git a/docs/_templates/autoapi/python/method.rst b/docs/_templates/autoapi/python/method.rst new file mode 100644 index 00000000..f50a1bd6 --- /dev/null +++ b/docs/_templates/autoapi/python/method.rst @@ -0,0 +1,19 @@ +{%- if obj.display %} +.. py:method:: {{ obj.short_name }}({{ obj.args }}){% if obj.return_annotation is not none %} -> {{ obj.return_annotation }}{% endif %} + +{% for (args, return_annotation) in obj.overloads %} + {{ obj.short_name }}({{ args }}){% if return_annotation is not none %} -> {{ return_annotation }}{% endif %} + +{% endfor %} + {% if obj.properties %} + {% for property in obj.properties %} + :{{ property }}: + {% endfor %} + + {% else %} + + {% endif %} + {% if obj.docstring %} + {{ obj.docstring|indent(3) }} + {% endif %} +{% endif %} diff --git a/docs/_templates/autoapi/python/module.rst b/docs/_templates/autoapi/python/module.rst new file mode 100644 index 00000000..ed0eb092 --- /dev/null +++ b/docs/_templates/autoapi/python/module.rst @@ -0,0 +1,104 @@ +{% if not obj.display %} +:orphan: + +{% endif %} +:py:mod:`{{ obj.name }}` +=========={{ "=" * obj.name|length }} + +.. py:module:: {{ obj.name }} + +{% if obj.docstring %} +.. autoapi-nested-parse:: + + {{ obj.docstring|indent(3) }} + +{% endif %} + +{% block subpackages %} +{% set visible_subpackages = obj.subpackages|selectattr("display")|list %} +{% if visible_subpackages %} + +{% endif %} +{% endblock %} +{% block submodules %} +{% set visible_submodules = obj.submodules|selectattr("display")|list %} +{% if visible_submodules %} +Submodules +---------- +.. toctree:: + :titlesonly: + :maxdepth: 1 + +{% for submodule in visible_submodules %} + {{ submodule.short_name }}/index.rst +{% endfor %} + + +{% endif %} +{% endblock %} +{% block content %} +{% if obj.all is not none %} +{% set visible_children = obj.children|selectattr("short_name", "in", obj.all)|list %} +{% elif obj.type is equalto("package") %} +{% set visible_children = obj.children|selectattr("display")|list %} +{% else %} +{% set visible_children = obj.children|selectattr("display")|rejectattr("imported")|list %} +{% endif %} +{% if visible_children %} +{{ obj.type|title }} Contents +{{ "-" * obj.type|length }}--------- + +{% set visible_classes = visible_children|selectattr("type", "equalto", "class")|list %} +{% set visible_functions = visible_children|selectattr("type", "equalto", "function")|list %} +{% set visible_attributes = visible_children|selectattr("type", "equalto", "data")|list %} +{% if "show-module-summary" in autoapi_options and (visible_classes or visible_functions) %} +{% block classes scoped %} +{% if visible_classes %} +Classes +~~~~~~~ + +.. autoapisummary:: + +{% for klass in visible_classes %} + {{ klass.id }} +{% endfor %} + + +{% endif %} +{% endblock %} + +{% block functions scoped %} +{% if visible_functions %} +Functions +~~~~~~~~~ + +.. autoapisummary:: + +{% for function in visible_functions %} + {{ function.id }} +{% endfor %} + + +{% endif %} +{% endblock %} + +{% block attributes scoped %} +{% if visible_attributes %} +Attributes +~~~~~~~~~~ + +.. autoapisummary:: + +{% for attribute in visible_attributes %} + {{ attribute.id }} +{% endfor %} + + +{% endif %} +{% endblock %} +{% endif %} +{% for obj_item in visible_children %} +{{ obj_item.render()|indent(0) }} +{% endfor %} +{% endif %} +{% endblock %} diff --git a/docs/_templates/autoapi/python/package.rst b/docs/_templates/autoapi/python/package.rst new file mode 100644 index 00000000..fb9a6496 --- /dev/null +++ b/docs/_templates/autoapi/python/package.rst @@ -0,0 +1 @@ +{% extends "python/module.rst" %} diff --git a/docs/_templates/autoapi/python/property.rst b/docs/_templates/autoapi/python/property.rst new file mode 100644 index 00000000..70af2423 --- /dev/null +++ b/docs/_templates/autoapi/python/property.rst @@ -0,0 +1,15 @@ +{%- if obj.display %} +.. py:property:: {{ obj.short_name }} + {% if obj.annotation %} + :type: {{ obj.annotation }} + {% endif %} + {% if obj.properties %} + {% for property in obj.properties %} + :{{ property }}: + {% endfor %} + {% endif %} + + {% if obj.docstring %} + {{ obj.docstring|indent(3) }} + {% endif %} +{% endif %} diff --git a/docs/_templates/badges.html b/docs/_templates/badges.html new file mode 100644 index 00000000..fe141b7c --- /dev/null +++ b/docs/_templates/badges.html @@ -0,0 +1,6 @@ +
+ Star +
+ + diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..dc83cab6 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,55 @@ +# docs/conf.py + +import os +import sys + +sys.path.insert(0, os.path.abspath("../")) + +project = "momentGW" +copyright = "2024, Booth Group" +author = "Booth Group" + +extensions = [ + "sphinx.ext.intersphinx", + "sphinx.ext.githubpages", + "sphinx.ext.mathjax", + "sphinx_mdinclude", + "sphinx_rtd_theme", + "autoapi.extension", + "sphinx.ext.napoleon", +] + +templates_path = ["_templates"] + +autoapi_dirs = ["../momentGW"] +autoapi_options = [ + "members", + "inherited-members", + "show-inheritance", +] +autoapi_member_order = "bysource" +autoapi_add_toctree_entry = False +autoapi_python_use_implicit_namespaces = True +autoapi_own_page_level = "class" +autoapi_template_dir = "_templates/autoapi" + +highlight_language = "python3" +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} +source_suffix = [".rst", ".md"] +master_doc = "index" + +html_theme = "sphinx_rtd_theme" +html_sidebars = { + "**": [ + "about.html", + "badges.html", + "navigation.html", + "relations.html", + "searchbox.html", + ] +} + +def setup(app): + """Setup function for Sphinx. + """ + pass diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..2932dd98 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,11 @@ +.. toctree:: + :maxdepth: 2 + :hidden: + + autoapi/momentGW/index.rst + autoapi/momentGW/uhf/index.rst + autoapi/momentGW/pbc/index.rst + autoapi/momentGW/pbc/uhf/index.rst + +.. mdinclude:: + ../README.md diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..32bb2452 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/momentGW/base.py b/momentGW/base.py index 99bb8796..e26bffbc 100644 --- a/momentGW/base.py +++ b/momentGW/base.py @@ -2,6 +2,8 @@ Base classes for moment-constrained GW solvers. """ +from collections import OrderedDict + import numpy as np from momentGW import init_logging, logging, mpi_helper, util @@ -10,7 +12,8 @@ class Base: """Base class.""" - _opts = [] + # Default options + _defaults = OrderedDict() def __init__( self, @@ -18,20 +21,22 @@ def __init__( mo_energy=None, mo_coeff=None, mo_occ=None, + frozen=None, **kwargs, ): + # Options + self._opts = self._defaults.copy() + for key, val in kwargs.items(): + if key not in self._opts: + raise AttributeError(f"{key} is not a valid option for {self.name}") + self._opts[key] = val + # Parameters self._scf = mf self._mo_energy = mo_energy self._mo_coeff = mo_coeff self._mo_occ = mo_occ - self.frozen = None - - # Options - for key, val in kwargs.items(): - if not hasattr(self, key): - raise AttributeError(f"{key} is not a valid option for {self.name}") - setattr(self, key, val) + self.frozen = frozen # Logging init_logging() @@ -86,20 +91,19 @@ def _check_modified(val, old): return val != old # Loop over options - for key in self._opts: + for key, val in self._opts.items(): if self._opt_is_used(key): - val = getattr(self, key) if isinstance(val, dict): # Format each entry of the dictionary keys, vals = zip(*val.items()) if val else ((), ()) - old = getattr(self.__class__, key) + old = self.__class__._defaults.get(key, None) keys = [f"{key}.{k}" for k in keys] mods = [old and _check_modified(v, old[k]) for k, v in val.items()] else: # Format the single value keys = [key] vals = [val] - mods = [_check_modified(val, getattr(self.__class__, key))] + mods = [_check_modified(val, self._defaults.get(key, None))] # Loop over entries for key, val, mod in zip(keys, vals, mods): @@ -265,6 +269,40 @@ def mo_occ(self, value): if value is not None: self._mo_occ = mpi_helper.bcast(np.asarray(value)) + def __getattr__(self, key): + """ + Try to get an attribute from the `_opts` dictionary. If it is + not found, raise an `AttributeError`. + + Parameters + ---------- + key : str + Attribute key. + + Returns + ------- + value : any + Attribute value. + """ + if key in self._defaults: + return self._opts[key] + raise AttributeError + + def __setattr__(self, key, val): + """ + Try to set an attribute from the `_opts` dictionary. If it is + not found, raise an `AttributeError`. + + Parameters + ---------- + key : str + Attribute key. + """ + if key in self._defaults: + self._opts[key] = val + else: + super().__setattr__(key, val) + class BaseGW(Base): """Base class for moment-constrained GW solvers. @@ -306,39 +344,27 @@ class BaseGW(Base): implementation requires a filepath to import the THC integrals. """ - # --- Default GW options - - diagonal_se = False - polarizability = "drpa" - npoints = 48 - optimise_chempot = False - fock_loop = False - fock_opts = dict( - fock_diis_space=10, - fock_diis_min_space=1, - conv_tol_nelec=1e-6, - conv_tol_rdm1=1e-8, - max_cycle_inner=50, - max_cycle_outer=20, - ) - compression = "ia" - compression_tol = 1e-10 - thc_opts = dict( - file_path=None, + _defaults = OrderedDict( + diagonal_se=False, + polarizability="drpa", + npoints=48, + optimise_chempot=False, + fock_loop=False, + fock_opts=OrderedDict( + fock_diis_space=10, + fock_diis_min_space=1, + conv_tol_nelec=1e-6, + conv_tol_rdm1=1e-8, + max_cycle_inner=50, + max_cycle_outer=20, + ), + compression="ia", + compression_tol=1e-10, + thc_opts=OrderedDict( + file_path=None, + ), ) - _opts = [ - "diagonal_se", - "polarizability", - "npoints", - "optimise_chempot", - "fock_loop", - "fock_opts", - "compression", - "compression_tol", - "thc_opts", - ] - def __init__(self, mf, **kwargs): super().__init__(mf, **kwargs) diff --git a/momentGW/bse.py b/momentGW/bse.py index 3fc84cf3..07891ae8 100644 --- a/momentGW/bse.py +++ b/momentGW/bse.py @@ -3,6 +3,8 @@ constraints for molecular systems. """ +from collections import OrderedDict + import numpy as np from dyson import CPGF, MBLGF @@ -70,12 +72,11 @@ class BSE(Base): Default value is `"singlet"`. """ - # --- Default BSE options - - excitation = "singlet" - polarizability = None - - _opts = Base._opts + ["excitation", "polarizability"] + _defaults = OrderedDict( + **Base._defaults, + excitation="singlet", + polarizability=None, + ) _kernel = kernel @@ -487,13 +488,12 @@ class cpBSE(BSE): Default value is `"singlet"`. """ - # --- Extra cpBSE options - - scale = None - grid = None - eta = 0.1 - - _opts = BSE._opts + ["scale", "grid", "eta"] + _defaults = OrderedDict( + **BSE._defaults, + scale=None, + grid=None, + eta=0.1, + ) def __init__(self, gw, **kwargs): super().__init__(gw, **kwargs) diff --git a/momentGW/evgw.py b/momentGW/evgw.py index 7a39d6e7..6cda563f 100644 --- a/momentGW/evgw.py +++ b/momentGW/evgw.py @@ -3,6 +3,8 @@ constraints for molecular systems. """ +from collections import OrderedDict + import numpy as np from momentGW import logging, util @@ -186,29 +188,18 @@ class evGW(GW): which they are considered zero. Default value is `1e-11`. """ - # --- Extra evGW options - - g0 = False - w0 = False - max_cycle = 50 - conv_tol = 1e-8 - conv_tol_moms = 1e-6 - conv_logical = all - diis_space = 8 - damping = 0.0 - weight_tol = 1e-11 - - _opts = GW._opts + [ - "g0", - "w0", - "max_cycle", - "conv_tol", - "conv_tol_moms", - "conv_logical", - "diis_space", - "damping", - "weight_tol", - ] + _defaults = OrderedDict( + **GW._defaults, + g0=False, + w0=False, + max_cycle=50, + conv_tol=1e-8, + conv_tol_moms=1e-6, + conv_logical=all, + diis_space=8, + damping=0.0, + weight_tol=1e-11, + ) _kernel = kernel diff --git a/momentGW/fock.py b/momentGW/fock.py index 3bbc8487..d631aff4 100644 --- a/momentGW/fock.py +++ b/momentGW/fock.py @@ -2,6 +2,8 @@ Fock matrix self-consistent loop. """ +from collections import OrderedDict + import numpy as np import scipy from dyson import Lehmann @@ -146,26 +148,25 @@ def minimize_chempot(se, fock, nelec, occupancy=2, x0=0.0, tol=1e-6, maxiter=200 class BaseFockLoop: """Base class for Fock loops.""" - _opts = [] - - # --- Default Fock loop options - - fock_diis_space = 10 - fock_diis_min_space = 1 - conv_tol_nelec = 1e-6 - conv_tol_rdm1 = 1e-8 - max_cycle_inner = 100 - max_cycle_outer = 20 + _defaults = OrderedDict( + fock_diis_space=10, + fock_diis_min_space=1, + conv_tol_nelec=1e-6, + conv_tol_rdm1=1e-8, + max_cycle_inner=100, + max_cycle_outer=20, + ) def __init__(self, gw, gf=None, se=None, **kwargs): # Parameters self.gw = gw # Options + self._opts = self._defaults.copy() for key, val in kwargs.items(): - if not hasattr(self, key): + if key not in self._opts: raise AttributeError(f"{key} is not a valid option for {self.name}") - setattr(self, key, val) + self._opts[key] = val # Attributes self._h1e = None @@ -396,6 +397,40 @@ def nocc(self): """Get the number of occupied MOs.""" return self.gw.nocc + def __getattr__(self, key): + """ + Try to get an attribute from the `_opts` dictionary. If it is + not found, raise an `AttributeError`. + + Parameters + ---------- + key : str + Attribute key. + + Returns + ------- + value : any + Attribute value. + """ + if key in self._defaults: + return self._opts[key] + raise AttributeError + + def __setattr__(self, key, val): + """ + Try to set an attribute from the `_opts` dictionary. If it is + not found, raise an `AttributeError`. + + Parameters + ---------- + key : str + Attribute key. + """ + if key in self._defaults: + self._opts[key] = val + else: + super().__setattr__(key, val) + class FockLoop(BaseFockLoop): """ diff --git a/momentGW/fsgw.py b/momentGW/fsgw.py index da0f9ed7..fc6001dc 100644 --- a/momentGW/fsgw.py +++ b/momentGW/fsgw.py @@ -3,6 +3,9 @@ constraints for molecular systems. """ +import copy +from collections import OrderedDict + import numpy as np from momentGW import logging, mpi_helper, util @@ -67,8 +70,9 @@ def kernel( # Get the solver solver_options = {} if not gw.solver_options else gw.solver_options.copy() - for key in gw.solver._opts: - solver_options[key] = solver_options.get(key, getattr(gw, key, getattr(gw.solver, key))) + for key in gw.solver._defaults: + if key not in solver_options: + solver_options[key] = copy.deepcopy(gw._opts.get(key, gw.solver._defaults[key])) with logging.with_silent(): subgw = gw.solver(gw._scf, **solver_options) subgw.frozen = gw.frozen @@ -192,32 +196,19 @@ class fsGW(GW): empty `dict`. """ - # --- Default fsGW options - - fock_loop = True - optimise_chempot = True - - # --- Extra fsGW options - - max_cycle = 50 - conv_tol = 1e-8 - conv_tol_moms = 1e-8 - conv_logical = all - diis_space = 8 - damping = 0.0 - solver = GW - solver_options = {} - - _opts = GW._opts + [ - "max_cycle", - "conv_tol", - "conv_tol_moms", - "conv_logical", - "diis_space", - "damping", - "solver", - "solver_options", - ] + _defaults = OrderedDict( + **GW._defaults, + max_cycle=50, + conv_tol=1e-8, + conv_tol_moms=1e-8, + conv_logical=all, + diis_space=8, + damping=0.0, + solver=GW, + solver_options={}, + ) + _defaults["fock_loop"] = True + _defaults["optimise_chempot"] = True _kernel = kernel diff --git a/momentGW/pbc/base.py b/momentGW/pbc/base.py index 4ec417f9..2282ba94 100644 --- a/momentGW/pbc/base.py +++ b/momentGW/pbc/base.py @@ -3,8 +3,9 @@ conditions. """ +from collections import OrderedDict + import numpy as np -from pyscf.pbc.mp.kmp2 import get_nmo, get_nocc from momentGW import logging from momentGW.base import Base, BaseGW @@ -55,20 +56,11 @@ class BaseKGW(BaseGW): `False`. """ - # --- Default KGW options - - compression = None - - # --- Extra PBC options - - fc = False - - _opts = BaseGW._opts + [ - "fc", - ] - - get_nmo = get_nmo - get_nocc = get_nocc + _defaults = OrderedDict( + **BaseGW._defaults, + fc=False, + ) + _defaults["compression"] = None def __init__(self, mf, **kwargs): super().__init__(mf, **kwargs) diff --git a/momentGW/pbc/evgw.py b/momentGW/pbc/evgw.py index e5d34a25..8b4f44ca 100644 --- a/momentGW/pbc/evgw.py +++ b/momentGW/pbc/evgw.py @@ -83,7 +83,7 @@ class evKGW(KGW, evGW): which they are considered zero. Default value is `1e-11`. """ - _opts = util.list_union(KGW._opts, evGW._opts) + _defaults = util.dict_union(KGW._defaults, evGW._defaults) @property def name(self): diff --git a/momentGW/pbc/fsgw.py b/momentGW/pbc/fsgw.py index 6166bc53..4908ce95 100644 --- a/momentGW/pbc/fsgw.py +++ b/momentGW/pbc/fsgw.py @@ -79,11 +79,10 @@ class fsKGW(KGW, fsGW): empty `dict`. """ - # --- Default fsKGW options - - solver = KGW - - _opts = util.list_union(KGW._opts, fsGW._opts) + _defaults = util.dict_union(KGW._defaults, fsGW._defaults) + _defaults["fock_loop"] = True + _defaults["optimise_chempot"] = True + _defaults["solver"] = KGW project_basis = staticmethod(qsKGW.project_basis) self_energy_to_moments = staticmethod(qsKGW.self_energy_to_moments) diff --git a/momentGW/pbc/gw.py b/momentGW/pbc/gw.py index 13630c3c..f0991ea6 100644 --- a/momentGW/pbc/gw.py +++ b/momentGW/pbc/gw.py @@ -61,7 +61,7 @@ class KGW(BaseKGW, GW): `False`. """ - _opts = util.list_union(BaseKGW._opts, GW._opts) + _defaults = util.dict_union(BaseKGW._defaults, GW._defaults) @property def name(self): diff --git a/momentGW/pbc/qsgw.py b/momentGW/pbc/qsgw.py index 261ad755..39acf88d 100644 --- a/momentGW/pbc/qsgw.py +++ b/momentGW/pbc/qsgw.py @@ -98,11 +98,8 @@ class qsKGW(KGW, qsGW): empty `dict`. """ - # --- Default qsKGW options - - solver = KGW - - _opts = util.list_union(KGW._opts, qsGW._opts) + _defaults = util.dict_union(KGW._defaults, qsGW._defaults) + _defaults["solver"] = KGW check_convergence = evKGW.check_convergence diff --git a/momentGW/pbc/scgw.py b/momentGW/pbc/scgw.py index f88dd5e1..862f3f26 100644 --- a/momentGW/pbc/scgw.py +++ b/momentGW/pbc/scgw.py @@ -72,7 +72,7 @@ class scKGW(KGW, scGW): Damping parameter. Default value is `0.0`. """ - _opts = util.list_union(KGW._opts, scGW._opts) + _defaults = util.dict_union(KGW._defaults, scGW._defaults) check_convergence = evKGW.check_convergence remove_unphysical_poles = evKGW.remove_unphysical_poles diff --git a/momentGW/pbc/uhf/evgw.py b/momentGW/pbc/uhf/evgw.py index ff46a9a3..52db4a32 100644 --- a/momentGW/pbc/uhf/evgw.py +++ b/momentGW/pbc/uhf/evgw.py @@ -84,7 +84,7 @@ class evKUGW(KUGW, evKGW, evUGW): which they are considered zero. Default value is `1e-11`. """ - _opts = util.list_union(evKGW._opts, evKGW._opts, evUGW._opts) + _defaults = util.dict_union(evKGW._defaults, evKGW._defaults, evUGW._defaults) @property def name(self): diff --git a/momentGW/pbc/uhf/fsgw.py b/momentGW/pbc/uhf/fsgw.py index 5dead367..f01c4501 100644 --- a/momentGW/pbc/uhf/fsgw.py +++ b/momentGW/pbc/uhf/fsgw.py @@ -80,11 +80,10 @@ class fsKUGW(KUGW, fsKGW, fsUGW): empty `dict`. """ - # --- Default fsKUGW options - - solver = KUGW - - _opts = util.list_union(KUGW._opts, fsKGW._opts, fsUGW._opts) + _defaults = util.dict_union(KUGW._defaults, fsKGW._defaults, fsUGW._defaults) + _defaults["fock_loop"] = True + _defaults["optimise_chempot"] = True + _defaults["solver"] = KUGW project_basis = staticmethod(qsKUGW.project_basis) self_energy_to_moments = staticmethod(qsKUGW.self_energy_to_moments) diff --git a/momentGW/pbc/uhf/gw.py b/momentGW/pbc/uhf/gw.py index 40f93a24..646afde9 100644 --- a/momentGW/pbc/uhf/gw.py +++ b/momentGW/pbc/uhf/gw.py @@ -61,7 +61,7 @@ class KUGW(BaseKUGW, KGW, UGW): `False`. """ - _opts = util.list_union(BaseKUGW._opts, KGW._opts, UGW._opts) + _defaults = util.dict_union(BaseKUGW._defaults, KGW._defaults, UGW._defaults) @property def name(self): diff --git a/momentGW/pbc/uhf/qsgw.py b/momentGW/pbc/uhf/qsgw.py index 2a18b189..2aa40bc9 100644 --- a/momentGW/pbc/uhf/qsgw.py +++ b/momentGW/pbc/uhf/qsgw.py @@ -99,11 +99,8 @@ class qsKUGW(KUGW, qsKGW, qsUGW): empty `dict`. """ - # --- Default qsKUGW options - - solver = KUGW - - _opts = util.list_union(KUGW._opts, qsKGW._opts, qsUGW._opts) + _defaults = util.dict_union(KUGW._defaults, qsKGW._defaults, qsUGW._defaults) + _defaults["solver"] = KUGW check_convergence = evKUGW.check_convergence diff --git a/momentGW/pbc/uhf/scgw.py b/momentGW/pbc/uhf/scgw.py index 9b645598..9703e577 100644 --- a/momentGW/pbc/uhf/scgw.py +++ b/momentGW/pbc/uhf/scgw.py @@ -73,7 +73,7 @@ class scKUGW(KUGW, scKGW, scUGW): Damping parameter. Default value is `0.0`. """ - _opts = util.list_union(scKGW._opts, scKGW._opts, scUGW._opts) + _defaults = util.dict_union(scKGW._defaults, scKGW._defaults, scUGW._defaults) check_convergence = evKUGW.check_convergence remove_unphysical_poles = evKUGW.remove_unphysical_poles diff --git a/momentGW/qsgw.py b/momentGW/qsgw.py index d09e806c..16cb16b6 100644 --- a/momentGW/qsgw.py +++ b/momentGW/qsgw.py @@ -3,6 +3,9 @@ constraints for molecular systems. """ +import copy +from collections import OrderedDict + import numpy as np from pyscf import lib @@ -76,8 +79,9 @@ def kernel( # Get the solver solver_options = {} if not gw.solver_options else gw.solver_options.copy() - for key in gw.solver._opts: - solver_options[key] = solver_options.get(key, getattr(gw, key, getattr(gw.solver, key))) + for key in gw.solver._defaults: + if key not in solver_options: + solver_options[key] = copy.deepcopy(gw._opts.get(key, gw.solver._defaults[key])) with logging.with_silent(): subgw = gw.solver(gw._scf, **solver_options) subgw.frozen = gw.frozen @@ -252,37 +256,22 @@ class qsGW(GW): empty `dict`. """ - # --- Extra qsGW options - - max_cycle = 50 - max_cycle_qp = 50 - conv_tol = 1e-8 - conv_tol_moms = 1e-6 - conv_tol_qp = 1e-8 - conv_logical = all - diis_space = 8 - diis_space_qp = 8 - damping = 0.0 - eta = 1e-1 - srg = 0.0 - solver = GW - solver_options = None - - _opts = GW._opts + [ - "max_cycle", - "max_cycle_qp", - "conv_tol", - "conv_tol_moms", - "conv_tol_qp", - "conv_logical", - "diis_space", - "diis_space_qp", - "damping", - "eta", - "srg", - "solver", - "solver_options", - ] + _defaults = OrderedDict( + **GW._defaults, + max_cycle=50, + max_cycle_qp=50, + conv_tol=1e-8, + conv_tol_moms=1e-6, + conv_tol_qp=1e-8, + conv_logical=all, + diis_space=8, + diis_space_qp=8, + damping=0.0, + eta=1e-1, + srg=0.0, + solver=GW, + solver_options={}, + ) _kernel = kernel diff --git a/momentGW/uhf/evgw.py b/momentGW/uhf/evgw.py index d3fd84f9..2715c07d 100644 --- a/momentGW/uhf/evgw.py +++ b/momentGW/uhf/evgw.py @@ -80,7 +80,7 @@ class evUGW(UGW, evGW): which they are considered zero. Default value is `1e-11`. """ - _opts = util.list_union(UGW._opts, evGW._opts) + _defaults = util.dict_union(UGW._defaults, evGW._defaults) @property def name(self): diff --git a/momentGW/uhf/fsgw.py b/momentGW/uhf/fsgw.py index a8f5060b..a9cc5a33 100644 --- a/momentGW/uhf/fsgw.py +++ b/momentGW/uhf/fsgw.py @@ -76,11 +76,10 @@ class fsUGW(UGW, fsGW): empty `dict`. """ - # --- Default fsUGW options - - solver = UGW - - _opts = util.list_union(UGW._opts, fsGW._opts) + _defaults = util.dict_union(UGW._defaults, fsGW._defaults) + _defaults["fock_loop"] = True + _defaults["optimise_chempot"] = True + _defaults["solver"] = UGW project_basis = staticmethod(qsUGW.project_basis) self_energy_to_moments = staticmethod(qsUGW.self_energy_to_moments) diff --git a/momentGW/uhf/qsgw.py b/momentGW/uhf/qsgw.py index 48b62160..81fa02e0 100644 --- a/momentGW/uhf/qsgw.py +++ b/momentGW/uhf/qsgw.py @@ -94,11 +94,8 @@ class qsUGW(UGW, qsGW): empty `dict`. """ - # --- Default qsUGW options - - solver = UGW - - _opts = util.list_union(UGW._opts, qsGW._opts) + _defaults = util.dict_union(UGW._defaults, qsGW._defaults) + _defaults["solver"] = UGW check_convergence = evUGW.check_convergence diff --git a/momentGW/uhf/scgw.py b/momentGW/uhf/scgw.py index 380ae3e9..b5098e77 100644 --- a/momentGW/uhf/scgw.py +++ b/momentGW/uhf/scgw.py @@ -68,7 +68,7 @@ class scUGW(UGW, scGW): Damping parameter. Default value is `0.0`. """ - _opts = util.list_union(UGW._opts, scGW._opts) + _defaults = util.dict_union(UGW._defaults, scGW._defaults) check_convergence = evUGW.check_convergence remove_unphysical_poles = evUGW.remove_unphysical_poles diff --git a/momentGW/util.py b/momentGW/util.py index 6c62db0a..7c0ffaf4 100644 --- a/momentGW/util.py +++ b/momentGW/util.py @@ -266,6 +266,31 @@ def list_union(*args): return out +def dict_union(*args): + """ + Find the union of a list of dictionaries, preserving the order + of the first occurrence of each key. + + Parameters + ---------- + args : list of dict + Dictionaries to find the union of. + + Returns + ------- + out : dict + Union of the dictionaries. + """ + cache = set() + out = type(args[0])() if len(args) else {} + for arg in args: + for x in arg: + if x not in cache: + cache.add(x) + out[x] = arg[x] + return out + + def build_1h1p_energies(mo_energy, mo_occ): r""" Construct an array of 1h1p energies where elements are diff --git a/pyproject.toml b/pyproject.toml index 419ebf07..34061f5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,10 @@ dev = [ "pytest>=6.2.4", "pytest-cov>=4.0.0", "pytest-env>=1.1.0", + "sphinx>=4.0.0", + "sphinx-mdinclude>=0.5.0", + "sphinx_rtd_theme>=1.0.0", + "sphinx-autoapi>=1.8.0", ] mpi = [ "mpi4py>=3.1.0",