Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions docs/toolbox/basis/basis.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
Basis optimization
==================

.. currentmodule:: sisl_toolbox.siesta.minimizer

Optimizing a basis set for SIESTA is a cumbersome task.
The goal of this toolbox is to allow users to **optimize a basis set with just one CLI command.**

The commands and their options can be accessed like:

.. code-block:: bash

stoolbox basis --help

In summary, whenever one wants to optimize a basis set for a given system,
the first step is to create a directory with the input files to run the
calculation. This directory should contain, as usual:

- The ``.fdf`` files with all the input parameters for the calculation.
- The pseudopotentials (``.psf`` or ``.psml`` files).

Then, one can directly run the optimization:

.. code-block:: bash

stoolbox basis optim --geometry input.fdf

with ``input.fdf`` being the input file for the calculation. This will use all the default
values. Since there are many possible tweaks, we invite you to read carefully the help
message from the CLI. Here we will just mention some important things that could go unnoticed.

**Basis enthalpy:** The quantity that is minimized is the basis enthalpy. This is :math:`H = E + pV`
with :math:`E` being the energy of the system, :math:`V` the volume of the basis and :math:`p` a "pressure" that
is defined in the fdf file with the ```BasisPressure`` table. This "pressure" penalizes bigger
basis, which result in more expensive calculations. It is the responsibility of the user to
set this value. As a rule of thumb, we recommend to set it to ``0.02 GPa`` for the first two
rows of the periodic table and ``0.2 GPa`` for the rest.

**The SIESTA command:** There is a ``--siesta-cmd`` option to specify the way of executing SIESTA. By default, it
is simply calling the ``siesta`` command, but you could set it for example to ``mpirun -n 4 siesta``
so that SIESTA is ran in parallel.

There is no problem with using this CLI in clusters inside a submitted job, for example.

**A custom basis definition:** It may happen that the conventional optimizable parameters as well as their lower and
upper bounds are not good for your case (e.g. you would like the upper bound for a cutoff
radius to be higher). In that case, you can create a custom ``--basis-spec``. The best way
to do it is by calling

.. code-block:: bash

stoolbox basis build --geometry input.fdf

which will generate a yaml file with a basis specification that you can tweak manually.
Then, you can pass it directly to the optimization using the ``--config`` option:

.. code-block:: bash

stoolbox basis optim --geometry input.fdf --config my_config.yaml

**Installing the optimizers:** The default optimizer is BADS (https://github.com/acerbilab/bads)
which is the one that we have found works best to optimize basis sets. The optimizer is however
not installed by default. You can install it using pip:

.. code-block:: bash

pip install pybads

and the same would apply for other optimizers that you may want to use.

**Output:** The output that appears on the terminal is left to the particular optimizer.
However, sisl generates ``.dat`` files which contain information about each SIESTA execution.
These files contain one column for each variable being optimized and one column for the
metric to minimize.


Python API
----------

The functions that do the work are also usable in python code by importing them:

.. code-block:: python

from sisl_toolbox.siesta.minimizer import optimize_basis, write_basis_to_yaml

Here is their documentation:

.. autosummary::
:toctree: generated/
:nosignatures:

optimize_basis
write_basis_to_yaml
4 changes: 3 additions & 1 deletion docs/toolbox/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ Toolboxes should be imported directly.
The implemented toolboxes are listed here:

.. toctree::
:maxdepth: 1

transiesta/ts_fft
siesta/atom_plot
btd/btd
siesta/minimizer
basis/basis
33 changes: 33 additions & 0 deletions docs/toolbox/siesta/minimizer.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Minimizers
=================

.. currentmodule:: sisl_toolbox.siesta.minimizer

In `sisl_toolbox.siesta.minimizer` there is a collection of minimizers
that given some `variables`, a `runner` and a `metric` optimizes the
variables to minimize the metric.

These are the minimizer classes implemented:

.. autosummary::
:toctree: generated/
:nosignatures:

BaseMinimize
LocalMinimize
DualAnnealingMinimize
BADSMinimize
ParticleSwarmsMinimize

For each of them, there is a subclass particularly tailored to optimize
SIESTA runs:

.. autosummary::
:toctree: generated/
:nosignatures:

MinimizeSiesta
LocalMinimizeSiesta
DualAnnealingMinimizeSiesta
BADSMinimizeSiesta
ParticleSwarmsMinimizeSiesta
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ keywords = [
# Now all dependencies that were released around the same time should be the
# lower bound of dependencies.
dependencies = [
"pyyaml",
# We need npt.NDArray
"numpy>=1.21",
"scipy>=1.6",
Expand Down
217 changes: 213 additions & 4 deletions src/sisl/_lib/_argparse.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,220 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

import argparse
import inspect
import typing
from typing import Any, Callable, Literal, Optional, Union

from sisl._lib._docscrape import FunctionDoc

try:
from rich_argparse import RichHelpFormatter
from rich_argparse import RawTextRichHelpFormatter

SislHelpFormatter = RichHelpFormatter
SislHelpFormatter = RawTextRichHelpFormatter

Check warning on line 15 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L15

Added line #L15 was not covered by tests
except ImportError:
import argparse

SislHelpFormatter = argparse.RawDescriptionHelpFormatter


def is_optional(field):
"""Check whether the annotation for a parameter is an Optional type."""
return typing.get_origin(field) is Union and type(None) in typing.get_args(field)

Check warning on line 22 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L22

Added line #L22 was not covered by tests


def is_literal(field):
"""Check whether the annotation for a parameter is a Literal type."""
return typing.get_origin(field) is Literal

Check warning on line 27 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L27

Added line #L27 was not covered by tests


def get_optional_arg(field):
"""Get the type of an optional argument from the typehint.

It only works if the annotation only has one type.

E.g.: Optional[int] -> int
E.g.: Optional[Union[int, str]] -> raises ValueError
"""
if not is_optional(field):
return field

Check warning on line 39 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L38-L39

Added lines #L38 - L39 were not covered by tests

args = typing.get_args(field)
if len(args) > 2:
raise ValueError("Optional type must have at most 2 arguments")
for arg in args:
if arg is not type(None):
return arg

Check warning on line 46 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L41-L46

Added lines #L41 - L46 were not covered by tests

raise ValueError("No non-None type found in Union")

Check warning on line 48 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L48

Added line #L48 was not covered by tests


def get_literal_args(field):
"""Get the values of a literal.

E.g.: Literal[1, 2, 3] -> (1, 2, 3)
"""
return typing.get_args(field)

Check warning on line 56 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L56

Added line #L56 was not covered by tests


class NotPassedArg:
"""Placeholder to use for arguments that have not been passed.

By setting this as the default value for an argument, we can
later check if the argument was passed through the CLI or not.
"""

def __init__(self, val):
self.val = val

Check warning on line 67 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L67

Added line #L67 was not covered by tests

def __repr__(self):
return repr(self.val)

Check warning on line 70 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L70

Added line #L70 was not covered by tests

def __str__(self):
return str(self.val)

Check warning on line 73 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L73

Added line #L73 was not covered by tests

def __eq__(self, other):
return self.val == other

Check warning on line 76 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L76

Added line #L76 was not covered by tests

def __getattr__(self, name):
return getattr(self.val, name)

Check warning on line 79 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L79

Added line #L79 was not covered by tests


def get_runner(func):
"""Wraps a function to receive the args parsed from argparse"""

def _runner(args):

Check warning on line 85 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L85

Added line #L85 was not covered by tests

# Get the config argument. If present, load the arguments
# from the config (yaml) file.
config = getattr(args, "config", None)
config_args = {}
if config is not None:
import yaml

Check warning on line 92 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L89-L92

Added lines #L89 - L92 were not covered by tests

with open(args.config, "r") as f:
config_args = yaml.safe_load(f)

Check warning on line 95 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L94-L95

Added lines #L94 - L95 were not covered by tests

# Build the final arguments dictionary, using the arguments of the
# config file as defaults.
final_args = {}
for k, v in vars(args).items():
if k in ("runner", "config"):
continue
elif isinstance(v, NotPassedArg):
final_args[k] = config_args.get(k, v.val)

Check warning on line 104 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L99-L104

Added lines #L99 - L104 were not covered by tests
else:
final_args[k] = v

Check warning on line 106 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L106

Added line #L106 was not covered by tests

# And call the function
return func(**final_args)

Check warning on line 109 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L109

Added line #L109 was not covered by tests

return _runner

Check warning on line 111 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L111

Added line #L111 was not covered by tests


def get_argparse_parser(
func: Callable,
name: Optional[str] = None,
subp=None,
parser_kwargs: dict[str, Any] = {},
arg_aliases: dict[str, str] = {},
defaults: dict[str, Any] = {},
add_config: bool = True,
) -> argparse.ArgumentParser:
"""Creates an argument parser from a function's signature and docstring.

The created argument parser just mimics the function. It is a CLI version
of the function.

Parameters
----------
func :
The function to create the parser for.
name :
The name of the parser. If None, the function's name is used.
subp :
The subparser to add the parser to. If None, a new isolated
parser is created.
parser_kwargs :
Additional arguments to pass to the parser.
arg_aliases :
Dictionary holding aliases (shortcuts) for the arguments. The keys
of this dictionary are the argument names, and the values are the
aliases. For example, if the function has an argument called
`--size`, and you want to add a shortcut `-s`, you can pass
`arg_aliases={"size": "s"}`.
defaults :
Dictionary holding default values for the arguments. The keys
of this dictionary are the argument names, and the values are
the default values. The defaults are taken from the function's
signature if not specified.
add_config :
If True, adds a `--config` argument to the parser. This
argument accepts a path to a YAML file containing the
arguments for the function.
"""

# Check if the function needs to be added as a subparser
is_sub = not subp is None

Check warning on line 157 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L157

Added line #L157 was not covered by tests

# Get the function's information
fdoc = FunctionDoc(func)
signature = inspect.signature(func)

Check warning on line 161 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L160-L161

Added lines #L160 - L161 were not covered by tests

# Initialize parser
title = "".join(fdoc["Summary"])
parser_help = "\n".join(fdoc["Extended Summary"])
if is_sub:
p = subp.add_parser(

Check warning on line 167 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L164-L167

Added lines #L164 - L167 were not covered by tests
name or func.__name__.replace("_", "-"),
description=parser_help,
help=title,
**parser_kwargs,
)
else:
p = argparse.ArgumentParser(title, **parser_kwargs)

Check warning on line 174 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L174

Added line #L174 was not covered by tests

# Add the config argument to load the arguments from a YAML file
if add_config:
p.add_argument(

Check warning on line 178 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L177-L178

Added lines #L177 - L178 were not covered by tests
"--config",
"-c",
type=str,
default=None,
help="Path to a YAML file containing the arguments for the command",
)

group = p.add_argument_group("Function arguments")

Check warning on line 186 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L186

Added line #L186 was not covered by tests

# Add all the function's parameters to the parser
parameters_help = {p.name: p.desc for p in fdoc["Parameters"]}
for param in signature.parameters.values():

Check warning on line 190 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L189-L190

Added lines #L189 - L190 were not covered by tests

arg_names = [f"--{param.name.replace('_', '-')}"]
if param.name in arg_aliases:
arg_names.append(f"-{arg_aliases[param.name]}")

Check warning on line 194 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L192-L194

Added lines #L192 - L194 were not covered by tests

annotation = param.annotation
if is_optional(annotation):
annotation = get_optional_arg(annotation)

Check warning on line 198 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L196-L198

Added lines #L196 - L198 were not covered by tests

choices = None
if is_literal(annotation):
choices = get_literal_args(annotation)
annotation = type(choices[0])

Check warning on line 203 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L200-L203

Added lines #L200 - L203 were not covered by tests

group.add_argument(

Check warning on line 205 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L205

Added line #L205 was not covered by tests
*arg_names,
type=annotation,
default=NotPassedArg(param.default),
choices=choices,
action=argparse.BooleanOptionalAction if annotation is bool else None,
required=param.default is inspect._empty,
help="\n".join(parameters_help.get(param.name, [])),
)

if is_sub:
defaults = {"runner": get_runner(func), **defaults}

Check warning on line 216 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L215-L216

Added lines #L215 - L216 were not covered by tests

p.set_defaults(**defaults)

Check warning on line 218 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L218

Added line #L218 was not covered by tests

return p

Check warning on line 220 in src/sisl/_lib/_argparse.py

View check run for this annotation

Codecov / codecov/patch

src/sisl/_lib/_argparse.py#L220

Added line #L220 was not covered by tests
9 changes: 7 additions & 2 deletions src/sisl_toolbox/cli/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ def register(self, setup):
"""
self._cmds.append(setup)

def __call__(self, argv=None):

def get_parser(self):
# Create command-line
cmd = Path(sys.argv[0])
p = argparse.ArgumentParser(
Expand All @@ -70,6 +69,12 @@ def __call__(self, argv=None):
for cmd in self._cmds:
cmd(subp, parser_kwargs=dict(formatter_class=p.formatter_class))

return p

def __call__(self, argv=None):

p = self.get_parser()

args = p.parse_args(argv)
args.runner(args)

Expand Down
Loading
Loading