Skip to content

Commit

Permalink
Simpler Workflow instantiation (#158)
Browse files Browse the repository at this point in the history
### Workflow instantiation
Makes the creation of `Workflow` objects more flexible by accepting `kwargs` that can be workflow/calculator/plotting parameters e.g.

```
wf = SinglepointWorkflow(atoms, functional='ki', ecutwfc=40.0)
```

whereas previously this would have to be

```
wf = SinglepointWorkflow(atoms, parameters={'functional': 'ki}, master_calc_params={'kcp': {'ecutwfc': 40.0}, 'pw': {'ecutwfc': 40}})
```

Closes #145 

### Changes to running subworkflows
This PR also introduces the `@classmethod` `fromparent` to the `Workflow` class and deprecated `Workflow.wf_kwargs` and `Workflow.run_subworkflow()`. Previously to create and run a subworkflow one had to do something like

```
subwf = Workflow(**self.kwargs)
self.run_subworkflow(subwf)
```

but instead now we do

```
subwf = Workflow.fromparent(self)
subwf.run()
```

which is cleaner and more pythonic

### Documentation
Added the docstrings of the `Workflow` class to the docs module using `autodoc` and `numpydoc`. This starts to address (but does not fully resolve) #147
  • Loading branch information
elinscott authored Jul 14, 2022
1 parent ff0de12 commit 5b0bf6f
Show file tree
Hide file tree
Showing 21 changed files with 425 additions and 225 deletions.
2 changes: 0 additions & 2 deletions .mypy.ini

This file was deleted.

27 changes: 14 additions & 13 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,8 @@ def sys2file(capsys, tmp_path):
def water() -> Dict[str, Any]:
# water
return {'atoms': molecule('H2O', vacuum=5.0, pbc=False),
'master_calc_params': {'kcp': {'ecutwfc': 20.0, 'nbnd': 5}}}
'ecutwfc': 20.0,
'nbnd': 5} # master_calc_params': {'kcp': {'ecutwfc': 20.0, 'nbnd': 5}}}


@pytest.fixture
Expand All @@ -180,13 +181,13 @@ def silicon() -> Dict[str, Any]:
pdict = [{'fsite': [0.25, 0.25, 0.25], 'ang_mtm': 'sp3'}]
si_projs = ProjectionBlocks.fromprojections([pdict, pdict], fillings=[True, False], spins=[None, None], atoms=si)
return {'atoms': si,
'master_calc_params': {'kcp': {'ecutwfc': 40.0},
'pw': {'ecutwfc': 40.0, 'nbnd': 10},
'w90_emp': {'dis_froz_max': 10.6, 'dis_win_max': 16.9},
'ui': {'smooth_int_factor': 2},
'master_calc_params': {'pw': {'nbnd': 10},
'w90_emp': {'dis_froz_max': 10.6, 'dis_win_max': 16.9}
},
'plot_params': {'Emin': -10, 'Emax': 4, 'degauss': 0.5},
'projections': si_projs}
'projections': si_projs,
'ecutwfc': 40.0,
'smooth_int_factor': 2}


@pytest.fixture
Expand All @@ -210,9 +211,9 @@ def tio2() -> Dict[str, Any]:
atoms=atoms)

return {'atoms': atoms,
'master_calc_params': {'pw': {'ecutwfc': 40, 'nbnd': 34},
'kcp': {'ecutwfc': 40.0}},
'projections': projs}
'master_calc_params': {'pw': {'nbnd': 34}},
'projections': projs,
'ecutwfc': 40.0}


@pytest.fixture
Expand All @@ -224,10 +225,10 @@ def gaas() -> Dict[str, Any]:
spins=[None, None, None],
atoms=atoms)
return {'atoms': atoms,
'master_calc_params': {'kcp': {'ecutwfc': 40.0},
'pw': {'ecutwfc': 40.0, 'nbnd': 45},
'w90_emp': {'dis_froz_max': 14.6, 'dis_win_max': 18.6},
'ui': {'smooth_int_factor': 4},
'master_calc_params': {'pw': {'nbnd': 45},
'w90_emp': {'dis_froz_max': 14.6, 'dis_win_max': 18.6}
},
'ecutwfc': 40.0,
'smooth_int_factor': 4,
'plot_params': {'degauss': 0.5},
'projections': gaas_projs}
14 changes: 10 additions & 4 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
import sys

import sphinx_rtd_theme

# sys.path.insert(0, os.path.abspath('.'))
sys.path.append('..')


# -- Project information -----------------------------------------------------
Expand All @@ -33,8 +34,9 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ['recommonmark', 'sphinx.ext.mathjax', 'sphinx.ext.autosectionlabel', 'sphinxcontrib.bibtex',
'sphinx_toolbox.collapse']
extensions = ['recommonmark', 'sphinx.ext.mathjax', 'sphinx.ext.autosectionlabel',
'sphinxcontrib.bibtex', 'sphinx_toolbox.collapse', 'sphinx.ext.autodoc',
'numpydoc', 'sphinx.ext.autosummary']
bibtex_bibfiles = ['refs.bib']
autosectionlabel_prefix_document = True

Expand Down Expand Up @@ -63,3 +65,7 @@
html_theme_options = {'logo_only': True, 'display_version': False}

master_doc = 'index'

# -- Autodoc options ----------------------------------------------------------
autodoc_typehints = 'none'
numpydoc_show_class_members = False
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Contents
theory
installation
running
modules
tutorials
links
references
Expand Down
13 changes: 13 additions & 0 deletions docs/modules.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Modules
=======

In this section we document the various classes defined within the ``koopmans`` python package.

The workflow module
^^^^^^^^^^^^^^^^^^^

The central objects in ``koopmans`` are ``Workflow`` objects. We use these to define and run workflows, as described :ref:`here <running:Running via python>`. The workflow that runs most calculations is the ``SinglepointWorkflow``, defined as follows.


.. autoclass:: koopmans.workflows.SinglepointWorkflow

2 changes: 2 additions & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
sphinx>=3.0
sphinxcontrib-bibtex>=2.1.4
sphinx_toolbox>=2.5.0
sphinx_rtd_theme>=1.0.0
recommonmark>=0.7.1
numpydoc>=1.4.0
45 changes: 36 additions & 9 deletions docs/running.rst
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
Running
=======
To run a calculation, all that is required is
How to run
==========
To run a calculation from the command line, all that is required is

.. code-block:: bash
$ koopmans <seed>.json
where ``<seed>.json`` is a ``koopmans`` input file.

The input file
^^^^^^^^^^^^^^
The input file is a JSON-formatted file that contains the calculation parameters, divided into the :ref:`workflow <running/workflow:The workflow block>`, :ref:`setup <running/setup:The setup block>`, :ref:`kcp <running/kcp:The kcp block>`, :ref:`pw <running/pw:The pw block>`, :ref:`w90 <running/w90:The w90 block>`, :ref:`pw2wannier <running/pw2wannier:The pw2wannier block>`, and :ref:`ui <running/ui:The ui block>` blocks.

The input file is a JSON-formatted file that contains the calculation parameters, divided into various blocks


.. toctree::
:hidden:

running/workflow
running/setup
Expand All @@ -22,9 +25,33 @@ The input file is a JSON-formatted file that contains the calculation parameters
running/ui
running/plot

Environment variables
^^^^^^^^^^^^^^^^^^^^^
Running via python
^^^^^^^^^^^^^^^^^^
It is possible to run ``koopmans`` workflows from within python, bypassing the need for an input file entirely. To do this, all you need to do is create a ``SinglepointWorkflow`` object

.. code-block:: python
wf = SinglepointWorkflow(...)
and then simply call

.. code-block:: python
wf.run()
For details of how to initialize a workflow object, see the :ref:`workflow class documentation <modules:The workflow module>`. After a calculation has finished, you can access the individual calculations e.g.

.. code-block:: python
final_calc = wf.calculations[-1]
and fetch their results e.g.

.. code-block:: python
total_energy = final_calc.results['energy']
.. include:: ../README.rst
:start-line: 68
:end-line: 80
:start-line: 93
:end-line: 115

2 changes: 1 addition & 1 deletion koopmans/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
'Python module for running KI and KIPZ calculations with Quantum Espresso'
from pathlib import Path

__version__ = '0.5.0'
__version__ = '0.6.0'
base_directory = Path(__path__[0]).parent
12 changes: 6 additions & 6 deletions koopmans/calculators/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ class CalculatorExt():
'''

prefix: str = ''
results: dict
results: Dict[str, Any]
ext_in: str = ''
ext_out: str = ''

def __init__(self, skip_qc=False, **kwargs):
def __init__(self, skip_qc: bool = False, **kwargs: Any):
# Handle any recognized QE keywords passed as arguments
self.parameters.update(**kwargs)

Expand Down Expand Up @@ -195,7 +195,7 @@ def todict(self):
return dct

@classmethod
def fromdict(cls: Type[TCalc], dct: dict) -> TCalc:
def fromdict(cls: Type[TCalc], dct: Any) -> TCalc:
calc = cls(dct.pop('atoms'))
for k, v in dct.items():
setattr(calc, k.lstrip('_'), v)
Expand Down Expand Up @@ -248,12 +248,12 @@ def check_convergence(self) -> None:
'output file for more details')

@abstractmethod
def todict(self) -> dict:
def todict(self) -> Dict[str, Any]:
...

@classmethod
@abstractmethod
def fromdict(cls: Type[TCalcABC], dct: dict) -> TCalc:
def fromdict(cls: Type[TCalcABC], dct: Dict[str, Any]) -> TCalc:
...

@classmethod
Expand Down Expand Up @@ -317,7 +317,7 @@ def generate_band_structure(self):

class KCWannCalculator(CalculatorExt):
# Parent class for KCWHam, KCWScreen and Wann2KCW calculators
def is_complete(self):
def is_complete(self) -> bool:
return self.results.get('job_done', False)

@property
Expand Down
8 changes: 4 additions & 4 deletions koopmans/pseudopotentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ class Pseudopotential:
pseudos_directory = Path(__file__).parents[1] / 'pseudos'

# A database containing all the available pseudopotentials
pseudo_database = []
pseudo_database: List[Pseudopotential] = []
for pseudo_file in chain(pseudos_directory.rglob('*.UPF'), pseudos_directory.rglob('*.upf')):
name = pseudo_file.name
splitname = re.split(r'\.|_|-', name)[0]
element = splitname[0].upper() + splitname[1:].lower()
library = pseudo_file.parents[1].name
functional = pseudo_file.parent.name
citations = []
citations: List[str] = []

kwargs = {}
if library.startswith('sssp'):
Expand Down Expand Up @@ -85,7 +85,7 @@ def pseudos_library_directory(pseudo_library: str, base_functional: str) -> Path
return pseudos_directory / pseudo_library / base_functional


def fetch_pseudo(**kwargs):
def fetch_pseudo(**kwargs: Any) -> Pseudopotential:
matches = [psp for psp in pseudo_database if all([getattr(psp, k) == v for k, v in kwargs.items()])]
request_str = ', '.join([f'{k} = {v}' for k, v in kwargs.items()])
if len(matches) == 0:
Expand All @@ -103,7 +103,7 @@ def read_pseudo_file(filename: Path) -> Dict[str, Any]:
'''

upf = upf_to_json(open(filename, 'r').read(), filename.name)
upf: Dict[str, Any] = upf_to_json(open(filename, 'r').read(), filename.name)

return upf['pseudo_potential']

Expand Down
2 changes: 1 addition & 1 deletion koopmans/settings/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def __setitem__(self, key: str, value: Any):
# Set the item
super().__setitem__(key, value)

def is_valid(self, name):
def is_valid(self, name: str) -> bool:
# Check if a keyword is valid. This is a separate subroutine to allow child classes to overwrite it
# e.g. QE calculators want to be able to set keywords such as Hubbard(i) where i is an arbitrary integer
return name in self.valid
Expand Down
21 changes: 9 additions & 12 deletions koopmans/testing/_stumble.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'''
Create a "stumbling" workflow that deliberately crashes the code after every single calculation and attempts to restart (for testing purposes)
Create a "stumbling" workflow that deliberately crashes the code after every single calculation and attempts to
restart (for testing purposes)
'''

from __future__ import annotations

import copy
from traceback import format_stack

from ase.calculators.calculator import CalculationFailed
from koopmans import workflows
Expand All @@ -25,11 +27,11 @@ def __init__(self, *args, **kwargs):
self.calc_counter = 1

@property
def calc_counter(self):
def calc_counter(self) -> int:
return self._calc_counter

@calc_counter.setter
def calc_counter(self, value):
def calc_counter(self, value: int):
self._calc_counter = value

def run_calculator_single(self, *args, **kwargs):
Expand All @@ -40,7 +42,7 @@ def run_calculator_single(self, *args, **kwargs):
super().run_calculator_single(*args, **kwargs)

def _run(self, *args, **kwargs):
if not self._is_a_subworkflow:
if self.parent is None:
# Create a copy of the state of the workflow before we start running it
dct_before_running = {k: copy.deepcopy(v) for k, v in self.__dict__.items() if k not in
['calc_counter']}
Expand All @@ -62,14 +64,9 @@ def _run(self, *args, **kwargs):
self._run(*args, **kwargs)
else:
# Prevent subworkflows from catching stumbles
self.calc_counter = self.parent.calc_counter
super()._run(*args, **kwargs)

def run_subworkflow(self, workflow, *args, **kwargs):
assert isinstance(workflow, StumblingWorkflow), \
'The subworkflow is not a Stumbling workflow; the monkeypatching must have failed'
workflow.calc_counter = self.calc_counter
super().run_subworkflow(workflow, *args, **kwargs)
self.calc_counter = workflow.calc_counter
self.parent.calc_counter = self.calc_counter


class StumblingSinglepointWorkflow(StumblingWorkflow, workflows.SinglepointWorkflow):
Expand Down
Loading

0 comments on commit 5b0bf6f

Please sign in to comment.