diff --git a/src/libnnp/version.h b/src/libnnp/version.h index b285a5af4..de90660dd 100644 --- a/src/libnnp/version.h +++ b/src/libnnp/version.h @@ -18,8 +18,8 @@ #define VERSION_H #define NNP_VERSION "2.0.0" -#define NNP_GIT_REV "98f117d8273b1747972a3ff1a66198defe1ad714" -#define NNP_GIT_REV_SHORT "98f117d" -#define NNP_GIT_BRANCH "master" +#define NNP_GIT_REV "62f67caed5fb85b19edf1be8ec565ea3568648d4" +#define NNP_GIT_REV_SHORT "62f67ca" +#define NNP_GIT_BRANCH "symfunc_paramgen" #endif diff --git a/tools/python/symfunc_paramgen/doc/Makefile b/tools/python/symfunc_paramgen/doc/Makefile new file mode 100644 index 000000000..2770436b2 --- /dev/null +++ b/tools/python/symfunc_paramgen/doc/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = symfunc_paramgen +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/tools/python/symfunc_paramgen/doc/README.md b/tools/python/symfunc_paramgen/doc/README.md new file mode 100644 index 000000000..083f2284d --- /dev/null +++ b/tools/python/symfunc_paramgen/doc/README.md @@ -0,0 +1,14 @@ +#### Building the documentation + +run + + make html + +to generate the documentation using sphinx. +This tool uses numpydoc-style docstrings. + +*Requires*: + +* Sphinx +* Sphinx alabaster theme (while the rtd theme might look nicer in principle, there seemed to be rendering issues when using it) +* numpydoc diff --git a/tools/python/symfunc_paramgen/doc/_static/readme.txt b/tools/python/symfunc_paramgen/doc/_static/readme.txt new file mode 100644 index 000000000..b64c312ca --- /dev/null +++ b/tools/python/symfunc_paramgen/doc/_static/readme.txt @@ -0,0 +1 @@ +Any static files might go here. diff --git a/tools/python/symfunc_paramgen/doc/code_structure.rst b/tools/python/symfunc_paramgen/doc/code_structure.rst new file mode 100644 index 000000000..5816cfcf6 --- /dev/null +++ b/tools/python/symfunc_paramgen/doc/code_structure.rst @@ -0,0 +1,10 @@ +module sfparamgen +--------------------------- + +.. currentmodule:: sfparamgen + +class SymFuncParamGenerator +=========================== +.. autoclass:: SymFuncParamGenerator + :members: + :undoc-members: \ No newline at end of file diff --git a/tools/python/symfunc_paramgen/doc/conf.py b/tools/python/symfunc_paramgen/doc/conf.py new file mode 100644 index 000000000..0b15dc7d2 --- /dev/null +++ b/tools/python/symfunc_paramgen/doc/conf.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# symfunc_paramgen documentation build configuration file, created by +# sphinx-quickstart on Wed May 8 15:37:02 2019. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. + +import os +import sys +sys.path.insert(0, os.path.abspath('../src/')) +sys.setrecursionlimit(1500) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.coverage', + 'sphinx.ext.mathjax', + 'sphinx.ext.viewcode', + 'sphinx.ext.autosummary', + 'numpydoc'] + # 'sphinx.ext.napoleon'] + +# TODO: remove if no longer needed with napoleon +# numpydoc_show_class_members = False + +# # Napoleon settings +# napoleon_google_docstring = False +# napoleon_numpy_docstring = True +# napoleon_include_private_with_doc = False +# napoleon_include_special_with_doc = True +# napoleon_use_admonition_for_examples = False +# napoleon_use_admonition_for_notes = False +# napoleon_use_admonition_for_references = False +# napoleon_use_ivar = False +# napoleon_use_param = True +# napoleon_use_rtype = False + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'symfunc_paramgen' +copyright = '2019, Florian Buchner' +author = 'Florian Buchner' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '1.0' +# The full version, including alpha/beta/rc tags. +release = '1.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' +# html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +html_theme_options = {'show_related': True} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# This is required for the alabaster theme +# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars +html_sidebars = { + '**': [ + 'relations.html', # needs 'show_related': True theme option to display + 'searchbox.html', + ] +} + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'symfunc_paramgendoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'symfunc_paramgen.tex', 'symfunc\\_paramgen Documentation', + 'Florian Buchner', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'symfunc_paramgen', 'symfunc_paramgen Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'symfunc_paramgen', 'symfunc_paramgen Documentation', + author, 'symfunc_paramgen', 'One line description of project.', + 'Miscellaneous'), +] + + + diff --git a/tools/python/symfunc_paramgen/doc/index.rst b/tools/python/symfunc_paramgen/doc/index.rst new file mode 100644 index 000000000..80ebed08d --- /dev/null +++ b/tools/python/symfunc_paramgen/doc/index.rst @@ -0,0 +1,39 @@ +.. symfunc_paramgen documentation master file, created by + sphinx-quickstart on Wed May 8 15:37:02 2019. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to symfunc_paramgen's documentation! +============================================ + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + +This tool generates sets of symmetry function parameters, according to rules proposed in the literature ([1]_, [2]_), and outputs +them in the format required by n2p2. + + + +Code documentation +================== + +.. toctree:: + :maxdepth: 2 + + code_structure + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + + +.. rubric:: References + +.. [1] https://doi.org/10.1063/1.5019667 +.. [2] https://doi.org/10.1063/1.5024611 + diff --git a/tools/python/symfunc_paramgen/doc/make.bat b/tools/python/symfunc_paramgen/doc/make.bat new file mode 100644 index 000000000..98281032d --- /dev/null +++ b/tools/python/symfunc_paramgen/doc/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=symfunc_paramgen + +if "%1" == "" goto help + +%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.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/tools/python/symfunc_paramgen/examples/example.ipynb b/tools/python/symfunc_paramgen/examples/example.ipynb new file mode 100644 index 000000000..96b0f93f6 --- /dev/null +++ b/tools/python/symfunc_paramgen/examples/example.ipynb @@ -0,0 +1,412 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Usage of Symmetry Function Parameter Generator" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Importing the tool" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append('../src')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from sfparamgen import SymFuncParamGenerator" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Basic usage overview" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Using this tool typically consists of the following steps:\n", + "\n", + "1. Creating an object of the SymFuncParamGenerator class.\n", + "2. Filling this object with the necessary settings and symmetry function parameters, using the class methods provided.\n", + "3. Writing the symmetry function parameters, in a format usable by n2p2.\n", + "\n", + "Steps 2. and 3. will most likely be repeated several times, with different settings, to create different symmetry function parameter sets." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1. Creating an object of the symmetry function parameter generator class" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We first need to instantiate the SymFuncParamGenerator class.\n", + "\n", + "The elements in the system, and the cutoff radius are required as arguments to the constructor. These are considered system parameters, rather than symmetry function settings, and as such are not intended for later change. To generate symmetry function parameters for different elements and/or for different cutoff radiuses, a new instance of the class should be created." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "myGenerator = SymFuncParamGenerator(elements=['S', 'Cu'], r_cutoff = 6.)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. Supplying the object with settings and generating the radial symmetry function parameters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "So far, we have a 'bare' symmetry function parameter generator object. We need to pass it the desired settings and tell it to generate actual symmetry function parameters. The different settings/method calls necessary for this are independent of each other and can be made in any order." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The symmetry function type needs to be specified. In this example, we will be setting it to 'radial':" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "myGenerator.symfunc_type = 'radial'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Importantly, we also need to generate a set of values for what we will call the 'radial parameters', r_shift and eta. We call them 'radial parameters' because they govern the sampling of radial space, but they are in fact required by any type of symmetry function, not only those of type 'radial'.\n", + "\n", + "The symmetry function parameter generator class provides a method for generating these parameters based on algorithms proposed in the literature. This method is in fact the central functionality of this tool.\n", + "\n", + "The method takes a few parameters. For explanation of these parameters and literature references, we refer to the documentation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "myGenerator.generate_radial_params(rule='imbalzano2018', mode='shift', nb_param_pairs=5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With this, everything is ready. We can, to check for correctness, or for future reference, display all current settings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "myGenerator.write_settings_overview()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3. Writing the symmetry function parameters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The object now contains a set of symmetry function parameters. We can now print these, in the format that the parameter file 'input.nn' used by n2p2 requires.\n", + "\n", + "(By default, they are written to stdout. See further down for how to write them to a file.)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "myGenerator.write_parameter_strings()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Repeating 2. and 3. with different settings" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Typically, you will want to create multiple symmetry function parameter sets, using different settings and symmetry function types. To do so, simply repeat steps 2. and 3. from above, with different settings.
\n", + "NB: Previously stored parameters are overwritten when setting new ones!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For this next example, let us now use one of the angular symmetry function types:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "myGenerator.symfunc_type = 'angular_narrow'" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Since we are now using an angular symmetry function type, we need to specify the additional parameter zetas, which was not needed in the example with a radial type from above:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "myGenerator.zetas = [1.0, 6.0]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For the sake of example, let us also generate new radial parameters, with different settings (although we could keep those from before):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "myGenerator.generate_radial_params(rule='gastegger2018', mode='center', nb_param_pairs=3, r_lower=1.5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now again print an overview of all settings used, as well as the final ready-for-use parameter strings:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "myGenerator.write_settings_overview()\n", + "myGenerator.write_parameter_strings()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Examples on usage of additional features" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Writing to a file" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the above examples, the settings overview and the parameter strings were written to stdout, which is the default behavior. You can, however, also write them to a file by passing a file object as an optional argument." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with open('example-outfile.txt', 'w') as f:\n", + " myGenerator.write_settings_overview(fileobj=f)\n", + " myGenerator.write_parameter_strings(fileobj=f)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Retrieving the element combinations on their own within Python" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Describing the environment of an atom in a multi-element system requires distinct symmetry functions corresponding to all possible central-atom-neighbor combinations.\n", + "\n", + "When outputting the symmetry function parameters the normal way, using the class' writing method, these element combinations are already taken care of. However, if needed, you can also access the combinations on their own via the dedicated member variable, as shown below.\n", + "\n", + "The element combinations are dependent on the symmetry function type, and as such are (re-)calculated and stored each time the symmetry function type is set." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "myGenerator.symfunc_type = 'radial'\n", + "print('radial:')\n", + "print(myGenerator.element_combinations)\n", + "\n", + "myGenerator.symfunc_type = 'angular_wide'\n", + "print('angular_wide:')\n", + "print(myGenerator.element_combinations)\n", + "\n", + "myGenerator.symfunc_type = 'angular_narrow'\n", + "print('angular_narrow:')\n", + "print(myGenerator.element_combinations)\n", + "\n", + "myGenerator.symfunc_type = 'weighted_angular'\n", + "print('weighted_angular:')\n", + "print(myGenerator.element_combinations)\n", + "\n", + "myGenerator.symfunc_type = 'weighted_radial'\n", + "print('weighted_radial:')\n", + "print(myGenerator.element_combinations)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Retrieving the radial parameters on their own within Python" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "Having generated sets of values for the 'radial parameters' r_shift and eta, you can retrieve them from within Python by accessing their member variables like so:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "myGenerator.generate_radial_params(rule='imbalzano2018', mode='shift', nb_param_pairs=5)\n", + "print(myGenerator.r_shift_grid)\n", + "print(myGenerator.eta_grid)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Setting custom radial parameters" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Instead of using the method for generating the 'radial parameters' r_shift and eta according to the schemes proposed in the literature, there is also the possibility to set custom values for these parameters.\n", + "\n", + "This is actually kind of bypassing the class' core functionality. But this way, you can still make use of the class' storage and writing functionalities, while using radial parameters of your own." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "myGenerator.set_custom_radial_params(r_shift_values=[1.1, 2.2, 3.3, 4.4], eta_values=[4.1, 3.2, 2.3, 1.4])\n", + "\n", + "myGenerator.write_settings_overview()\n", + "myGenerator.write_parameter_strings()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/tools/python/generate-acsf-angular_narrow.py b/tools/python/symfunc_paramgen/src/generate-acsf-angular_narrow.py similarity index 100% rename from tools/python/generate-acsf-angular_narrow.py rename to tools/python/symfunc_paramgen/src/generate-acsf-angular_narrow.py diff --git a/tools/python/generate-acsf-angular_wide.py b/tools/python/symfunc_paramgen/src/generate-acsf-angular_wide.py similarity index 100% rename from tools/python/generate-acsf-angular_wide.py rename to tools/python/symfunc_paramgen/src/generate-acsf-angular_wide.py diff --git a/tools/python/generate-acsf-radial.py b/tools/python/symfunc_paramgen/src/generate-acsf-radial.py similarity index 100% rename from tools/python/generate-acsf-radial.py rename to tools/python/symfunc_paramgen/src/generate-acsf-radial.py diff --git a/tools/python/symfunc_paramgen/src/sfparamgen.py b/tools/python/symfunc_paramgen/src/sfparamgen.py new file mode 100644 index 000000000..222326812 --- /dev/null +++ b/tools/python/symfunc_paramgen/src/sfparamgen.py @@ -0,0 +1,723 @@ +# -*- coding: utf-8 -*- + +import numpy as np +import sys +import inspect +import itertools +import warnings +from typing import Optional, TextIO + + +class SymFuncParamGenerator: + """Tools for generation, storage, and writing in the format required by + n2p2, of symmetry function parameter sets. + + Parameters + ---------- + elements : list of string + The chemical elements present in the system. + r_cutoff : float + Cutoff radius, at which symmetry functions go to zero. + Must be greater than zero. + + Attributes + ---------- + symfunc_type_numbers : dict + Dictionary mapping strings specifying the symmetry function type to the + numbers used internally by n2p2 to distinguish symmetry function types. + lambdas : numpy.ndarray + Set of values for the parameter lambda of angular symmetry functions. + Fixed to [-1, 1]. + radial_paramgen_settings : dict or None + Stores settings that were used in generating the symmetry function + parameters r_shift and eta using the class method provided for that + purpose. None, if no radial parameters have been generated yet, or if + custom ones (without using the method for generating radial parameters) + were set. + r_shift_grid + eta_grid + elements + element_combinations + r_cutoff + symfunc_type + zetas + """ + + symfunc_type_numbers = dict(radial=2, + angular_narrow=3, + angular_wide=9, + weighted_radial=12, + weighted_angular=13) + lambdas = np.array([-1.0, 1.0]) + + def __init__(self, elements, r_cutoff: float): + self._elements = elements + if not r_cutoff > 0: + raise ValueError('Invalid cutoff radius given. ' + 'Must be greater than zero.') + else: + self._r_cutoff = r_cutoff + + self._element_combinations = None + self._symfunc_type = None + self._zetas = None + + self._r_shift_grid = None + self._eta_grid = None + self.radial_paramgen_settings = None + + @property + def elements(self): + """The chemical elements present in the system (list of string, read-only). + """ + return self._elements + + @property + def element_combinations(self): + """Combinations of elements (list of tuple of string, read-only). + + This is (re)computed and set automatically each time + :py:attr:`~symfunc_type` is set. + """ + return self._element_combinations + + @property + def symfunc_type(self): + """Type of symmetry function for which parameters are to be generated + (`str`). + + When the setter for this is called it checks the validity of the given + symmetry function type. A symmetry function type is valid if it is in + the keys of the dict :py:attr:`~symfunc_type_numbers`, + and invalid otherwise. + + The setter also builds the necessary element combinations for the given + symmetry function type, and stores it to + :py:attr:`~element_combinations`. + + If the given symmetry function type is a radial one, + the setter also clears any preexisting zetas + (i.e., sets :py:attr:`~zetas` to None). + + Raises + ------ + ValueError + If invalid symmetry function type is given. + """ + return self._symfunc_type + + @symfunc_type.setter + def symfunc_type(self, value): + if value not in self.symfunc_type_numbers.keys(): + raise ValueError('Invalid symmetry function type. Must be one of ' + '{}'.format( + list(self.symfunc_type_numbers.keys()))) + else: + self._symfunc_type = value + # once symmetry function type has been set and found to be valid, + # build and store the element combinations + self._element_combinations = self.find_element_combinations() + # Clear any previous zeta values, if the given symfunc type is a + # radial one + if value in ['radial', 'weighted_radial']: + # set the member variable explicitly (with underscore) instead of + # calling setter, because the setter would make an array out of + # the None + self._zetas = None + + @property + def r_cutoff(self): + '''Cutoff radius where symmetry functions go to zero (float, read-only). + ''' + return self._r_cutoff + + @property + def zetas(self): + """Set of values for the parameter zeta of angular symmetry functions + (`numpy.ndarray`). + """ + return self._zetas + + @zetas.setter + def zetas(self, values): + # TODO: possibly add checks on values for zeta -> but how? + self._zetas = np.array(values) + + @property + def r_shift_grid(self): + """Set of values for the symmetry function parameter r_shift + (`numpy.ndarray` or None). + """ + return self._r_shift_grid + + @property + def eta_grid(self): + """Set of values for the symmetry function parameter eta + (`numpy.ndarray` or None). + """ + return self._eta_grid + + def check_symfunc_type(self, calling_method_name=None): + """Check if a symmetry function type has been set. + + Parameters + ---------- + calling_method_name : string, optional + The name of another method that calls this method. + If this parameter is given, a modified error message is printed + by this method, mentioning the method from which it was called. + This should make it clearer to the user in which part + of their own code to look for an error. + + Returns + ------- + None + + Raises + ------ + ValueError + If the symmetry function type has not been set (i.e., it is None). + """ + if self.symfunc_type is None: + if calling_method_name is None: + raise ValueError('Symmetry function type not set.') + else: + raise ValueError(f'Symmetry function type not set. ' + f'Calling method {calling_method_name} ' + f'requires that symmetry function type have ' + f'been set before.') + + def generate_radial_params(self, rule, mode, nb_param_pairs: int, + r_lower=None, r_upper=None): + """Generate a set of values for r_shift and eta. + + Such a set of (r_shift, eta)-values is required for any + symmetry function type (not only those called 'radial'). + Its generation is independent of the symmetry function type + and the angular symmetry function parameters zeta and lambda. + + Rules for parameter generation are implemented based on [1]_ and [2]_. + + The generated values are stored as arrays to :py:attr:`~r_shift_grid` + and :py:attr:`~eta_grid`. The entries are to be understood pairwise, + i.e., the i-th entry of :py:attr:`~r_shift_grid` and the i-th entry of + :py:attr:`~eta_grid` belong to one symmetry function. + Besides the values, the settings they were generated with are also + stored, to :py:attr:`~radial_paramgen_settings`. + + Parameters + ---------- + rule : {'gastegger2018', 'imbalzano2018'} + If rule=='gastegger2018' use the parameter generation rules + presented in [1]_. If rule=='imbalzano2018' use the parameter + generation rules presented in [2]_. + mode : {'center', 'shift'} + Selects which parameter generation procedure to use, on top of the + rule argument, since there are again two different varieties + presented in each of the two papers. 'center' sets r_shift to zero + for all symmetry functions, varying only eta. 'shift' creates + parameter sets where r_shift varies. The exact implementation + details differ depending on the rule parameter and are described in + the papers. + nb_param_pairs : int + Number of (r_shift, eta)-pairs to be generated. + r_lower : float + lowest value in the radial grid from which r_shift and eta values + are computed. + required if rule=='gastegger2018'. + ignored if rule=='imbalzano2018'. + r_upper : float, optional + Largest value in the radial grid from which r_shift and eta + values are computed. + Optional if rule=='gastegger2018', defaults to cutoff radius if not + given. + Ignored if rule=='imbalzano2018'. + + Returns + ------- + None + + Raises + ------ + ValueError + If nb_param_pairs is not two or greater. + TypeError + If parameter r_lower is not given, when using rule 'gastegger2018'. + ValueError + If illegal relation between r_lower, r_upper, r_cutoff. + ValueError + If invalid argument for parameters rule or mode. + + Notes + ----- + The parameter nb_param_pairs invariably specifies the number of + (r_shift, eta)-pairs ultimately generated, not the number of + intervals between points in a grid of r_shift values, or the + number of points in some auxiliary grid. + This constitutes a slight modification of the nomenclature in [2]_, + for the sake of consistent behavior across all options for rule and + mode. + + While the parameter generation by this method does not itself + depend on symmetry function type, be aware of the exact mathematical + expressions for the different symmetry function types and how the + parameters r_shift and eta appear slightly differently in each of them. + + All this method does is implement the procedures described in the two + papers for generating values of the symmetry function parameters + r_shift and eta. As far as this module is concerned, these can then + be used (with the above caveat) in combination with any symmetry + function type. However, this does not imply that their use with all + the different types of symmetry functions is actually discussed in + the papers or was necessarily intended by the authors. + + References + ---------- + .. [1] https://doi.org/10.1063/1.5019667 + .. [2] https://doi.org/10.1063/1.5024611 + """ + if not nb_param_pairs >= 2: + raise ValueError('nb_param_pairs must be two or greater.') + + # store those infos on radial parameter generation settings that are + # independent of the rule argument + self.radial_paramgen_settings = dict(rule=rule, + mode=mode, + nb_param_pairs=nb_param_pairs) + + r_cutoff = self.r_cutoff + + if rule == 'gastegger2018': + if r_lower is None: + raise TypeError('Argument r_lower is required for ' + 'rule "gastegger2018"') + if r_upper is None: + # by default, set largest value of radial grid to cutoff radius + r_upper = r_cutoff + + # store those settings that are unique to this rule + self.radial_paramgen_settings.update({'r_lower': r_lower, + 'r_upper': r_upper}) + + # create auxiliary grid + grid = np.linspace(r_lower, r_upper, nb_param_pairs) + + if mode == 'center': + # r_lower = 0 is not allowed in center mode, + # because it causes division by zero + if not 0 < r_lower < r_upper <= r_cutoff: + raise ValueError(f'Invalid argument(s): rule = {rule:s}, ' + f'mode = {mode:s} requires that 0 < ' + f'r_lower < r_upper <= r_cutoff.') + r_shift_grid = np.zeros(nb_param_pairs) + eta_grid = 1.0 / (2.0 * grid ** 2) + elif mode == 'shift': + # on the other hand, in shift mode, r_lower = 0 is possible + if not 0 <= r_lower < r_upper <= r_cutoff: + raise ValueError(f'Invalid argument(s): rule = {rule:s}, ' + f'mode = {mode:s} requires that 0 <= ' + f'r_lower < r_upper <= r_cutoff.') + r_shift_grid = grid + # compute the equidistant grid spacing + dr = (r_upper - r_lower) / (nb_param_pairs - 1) + eta_grid = np.full(nb_param_pairs, 1.0 / (2.0 * dr * dr)) + else: + raise ValueError('invalid argument for "mode"') + + elif rule == 'imbalzano2018': + if r_lower is not None: + this_method_name = inspect.currentframe().f_code.co_name + warnings.warn(f'The argument r_lower to method' + f' {this_method_name} will be ignored,' + f' since it is unused when calling the method' + f' with rule="imbalzano2018".') + if r_upper is not None: + this_method_name = inspect.currentframe().f_code.co_name + warnings.warn(f'The argument r_upper to method' + f' {this_method_name} will be ignored,' + f' since it is unused when calling the method' + f' with rule="imbalzano2018".') + + if mode == 'center': + nb_intervals = nb_param_pairs - 1 + gridpoint_indices = np.array(range(0, nb_intervals + 1)) + eta_grid = (nb_intervals ** (gridpoint_indices / nb_intervals) + / r_cutoff) ** 2 + r_shift_grid = np.zeros_like(eta_grid) + elif mode == 'shift': + # create extended auxiliary grid of r_shift values, + # that contains nb_param_pairs + 1 values + nb_intervals_extended = nb_param_pairs + gridpoint_indices_extended = np.array( + range(0, nb_intervals_extended + 1)) + rs_grid_extended = r_cutoff / nb_intervals_extended ** ( + gridpoint_indices_extended / nb_intervals_extended) + # from pairs of neighboring r_shift values, compute eta values. + # doing this for the nb_param_pairs + 1 values in the auxiliary + # grid ultimately gives nb_param_pairs different values for + # eta. + eta_grid = np.zeros(nb_param_pairs) + for idx in range(len(rs_grid_extended) - 1): + eta_current = 1 / (rs_grid_extended[idx] + - rs_grid_extended[idx + 1]) ** 2 + eta_grid[idx] = eta_current + # create final grid of r_shift values by excluding the first + # entry (for which r_shift coincides with the cutoff radius) + # from the extended grid + r_shift_grid = rs_grid_extended[1:] + # reverse the order of r_shift and eta values so they are + # sorted in order of ascending r_shift (not necessary, but + # makes the output consistent with the other options) + r_shift_grid = np.flip(r_shift_grid) + eta_grid = np.flip(eta_grid) + else: + raise ValueError('invalid argument for "mode"') + else: + raise ValueError('invalid argument for "rule"') + + # store the generated parameter sets + self._r_shift_grid = r_shift_grid + self._eta_grid = eta_grid + + def set_custom_radial_params(self, r_shift_values, eta_values): + """Set custom r_shift and eta, bypassing the class's generation method. + + The parameters r_shift_values and eta_values must have the same + length. + Their entries are to be understood pairwise, i.e., + the i-th entry of r_shift_values and the i-th entry of eta_values + belong together, describing one symmetry function. + + Parameters + ---------- + r_shift_values : sequence of float or 1D-array + Set of values for the symmetry function parameter r_shift. + eta_values : sequence of float or 1D-array + Set of values for the symmetry function parameter eta. + + Returns + ------- + None + + Raises + ------ + TypeError + If r_shift_values and eta_values do not have equal length. + ValueError + If there are negative entries in r_shift_values. + ValueError + If there are non-positive entries in eta_values. + + Notes + ----- + Setting r_shift and eta manually via this method instead of using the + method :py:attr:`~generate_radial_params` somewhat defeats the + purpose of the class as a generator of symmetry function parameter + values. However, it might still be useful, in case one wants to use + custom values for r_shift and eta, for which the generation is not + implemented as a class method, while still benefiting from the + parameter writing functionality of the class. + """ + + if len(r_shift_values) != len(eta_values): + raise TypeError('r_shift_values and eta_values must ' + 'have same length.') + if min(r_shift_values) < 0: + raise ValueError('r_shift_values must all be non-negative.') + if min(eta_values) <= 0: + raise ValueError('eta_values must all be greater than zero.') + # (re)set radial_paramgen_settings to None, indicating that custom + # values for (r_shift, eta) are used, rather than ones generated by + # class method. + self.radial_paramgen_settings = None + # set the values + self._r_shift_grid = np.array(r_shift_values) + self._eta_grid = np.array(eta_values) + + def check_writing_prerequisites(self, calling_method_name=None): + """Check if all data required for writing symmetry function sets are + present. + + | This comprises checking if the following have been set: + | - :py:attr:`~symfunc_type` + | - :py:attr:`~r_shift_grid` and :py:attr:`~eta_grid` + | - :py:attr:`~zetas`, if the symmetry function type is an angular one + + Parameters + ---------- + calling_method_name : string, optional + The name of another method that calls this method. + If this parameter is given, a modified error message is printed, + mentioning the method from which this error-raising method + was called. + This should make it clearer to a user in which part + of their code to look for an error. + + Returns + ------- + None + + Raises + ------ + ValueError + If values for r_shift or eta are not set. + ValueError + If an angular symmetry function type has been set, but no values + for zeta were set. + """ + self.check_symfunc_type(calling_method_name=calling_method_name) + + if calling_method_name is None: + if self._r_shift_grid is None or self._eta_grid is None: + raise ValueError('Values for r_shift and/or eta not set.') + if self.symfunc_type in ['angular_narrow', + 'angular_wide', + 'weighted_angular']: + if self.zetas is None: + raise ValueError( + f'Values for zeta not set (required for symmetry' + f' function type {self.symfunc_type}).\n' + f' If you are seeing this error despite having ' + f'previously set zetas, make sure\n' + f' they have not been cleared since by setting a ' + f'non-angular symmetry function type.') + else: + if self._r_shift_grid is None or self._eta_grid is None: + raise ValueError(f'Values for r_shift and/or eta not set. ' + f'Calling method {calling_method_name} ' + f'requires that values for r_shift and eta ' + f'have been set before.') + if self.symfunc_type in ['angular_narrow', + 'angular_wide', + 'weighted_angular']: + if self.zetas is None: + raise ValueError( + f'Values for zeta not set.\n ' + f'Calling {calling_method_name}, while using symmetry ' + f'function type {self.symfunc_type},\n' + f' requires zetas to have been set before.\n ' + f'If you are seeing this error despite having ' + f'previously set zetas, make sure\n' + f' they have not been cleared since by setting a ' + f'non-angular symmetry function type.') + + def write_settings_overview(self, fileobj: Optional[TextIO]=None): + """Write the settings the currently stored set of symmetry function + parameters was generated with. + + Parameters + ---------- + fileobj : `typing.TextIO`, optional + file object to write the settings information to. + If not given, write to sys.stdout instead. + + Returns + ------- + None + """ + this_method_name = inspect.currentframe().f_code.co_name + self.check_writing_prerequisites(calling_method_name=this_method_name) + + type_descriptions = dict(radial='Radial', + angular_narrow='Narrow angular', + angular_wide='Wide angular', + weighted_radial='Weighted radial', + weighted_angular='Weighted angular') + + if fileobj is None: + handle = sys.stdout + else: + handle = fileobj + + handle.write('########################################################' + '#################\n') + handle.write( + f'# {type_descriptions[self.symfunc_type]} symmetry function set, ' + f'for elements {self.elements}\n') + handle.write('########################################################' + '#################\n') + + handle.write(f'# r_cutoff = {self.r_cutoff}\n') + + # depending on whether radial parameters were generated using the + # method or custom-set (indicated by presence or absence of radial + # parameter generation settings), write the settings used or not + if self.radial_paramgen_settings is not None: + handle.write('# The following settings were used for generating ' + 'sets\n') + handle.write('# of values for the radial parameters r_shift and ' + 'eta:\n') + for key, value in self.radial_paramgen_settings.items(): + handle.write(f'# {key:14s} = {value}\n') + else: + handle.write('# A custom set of values was used for the radial ' + 'parameters r_shift and eta.\n') + handle.write('# Thus, there are no settings on radial parameter ' + 'generation available for display.\n') + + handle.write('# Sets of values for parameters:\n') + # set numpy print precision to lower number of decimal places for the + # following outputs + np.set_printoptions(precision=4) + + # printing numpy arrays causes linebreaks if they contain many entries. + # -> need to make sure that every single line in the output + # is prepended by "# " to make it into a comment. + outstring_r_shift = f'r_shift_grid = {self._r_shift_grid}' + handle.write('# ' + outstring_r_shift.replace("\n", "\n# ") + '\n') + outstring_eta = f'eta_grid = {self._eta_grid}' + handle.write('# ' + outstring_eta.replace("\n", "\n# ") + '\n') + + if self.symfunc_type in ['angular_narrow', + 'angular_wide', + 'weighted_angular']: + outstring_lambdas = f'lambdas = {self.lambdas}' + handle.write('# ' + outstring_lambdas.replace("\n", "\n# ") + '\n') + outstring_zetas = f'zetas = {self.zetas}' + handle.write('# ' + outstring_zetas.replace("\n", "\n# ") + '\n') + # reset numpy print precision to default + np.set_printoptions(precision=8) + handle.write('\n') + + def find_element_combinations(self): + """Create combinations of elements, depending on symmetry function type + and the elements in the system. + + For radial symmetry functions, the combinations are all possible + ordered pairs of elements in the system, including of an element with + itself. + For angular symmetry functions (narrow or wide), the combinations + consist of all possible elements as the central atom, and then again + for each central element all possible unordered pairs of neighbor + elements. + For weighted symmetry functions (radial or angular), the combinations + run only over all possible central elements, with neighbors not taken + into account at this stage. + + Returns + ------- + combinations : list of tuple of string + Each tuple in the list represents one element combination. Length + of the individual tuples can be 1, 2 or 3, depending on symmetry + function type. Zero-th entry of tuples is always the type of the + central atom, 1st and 2nd entry are neighbor atom types (radial sf: + one neighbor, angular sf: two neighbors, weighted sf: no neighbors) + """ + this_method_name = inspect.currentframe().f_code.co_name + self.check_symfunc_type(calling_method_name=this_method_name) + + combinations = [] + + if self.symfunc_type == 'radial': + for elem_central in self.elements: + for elem_neighbor in self.elements: + combinations.append((elem_central, elem_neighbor)) + elif self.symfunc_type in ['angular_narrow', 'angular_wide']: + for elem_central in self.elements: + for pair_of_neighbors in \ + itertools.combinations_with_replacement(self.elements, + 2): + comb = (elem_central,) + pair_of_neighbors + combinations.append(comb) + elif self.symfunc_type in ['weighted_radial', 'weighted_angular']: + for elem_central in self.elements: + combinations.append((elem_central,)) + + return combinations + + def write_parameter_strings(self, fileobj: Optional[TextIO]=None): + """Write symmetry function parameter sets, formatted as n2p2 requires. + + The output format is that required by the parameter file 'input.nn' + used by n2p2. The output is intended to be pasted/written to that + file. + + Each line in the output corresponds to one symmetry function. + + Output is formatted in blocks separated by blank lines, each block + corresponding to one element combination. The different blocks differ + from each other only in the element combinations and are otherwise + the same. + + Within each block, all combinations of the other parameters + r_shift, eta, lambda, zeta (the latter two only for angular + symmetry function types), are iterated over. + Note, however, that the value pairs for r_shift and eta are not + all the possible combinations of elements in r_shift_grid and eta_grid, + but only the combinations of the i-th entries of r_shift_grid with + the i-th entries of eta_grid. + + Schematic example: When r_shift_grid = [1, 2], eta_grid = [3, 4], + zetas = [5, 6], lambdas = [-1, 1] (the latter not being intended to be + set by the user, anyway), within each block of the output, the + method iterates over the following combinations of + (r_shift, eta, zeta, lambda): + + | (1, 3, 5, -1) + | (1, 3, 5, 1) + | (1, 3, 6, -1) + | (1, 3, 6, 1) + | (2, 4, 5, -1) + | (2, 4, 5, 1) + | (2, 4, 6, -1) + | (2, 4, 6, 1) + + Parameters + ---------- + fileobj : `typing.TextIO`, optional + file object to write the parameter strings to. + If not given, write to sys.stdout instead. + + Returns + ------- + None + """ + this_method_name = inspect.currentframe().f_code.co_name + self.check_writing_prerequisites(calling_method_name=this_method_name) + + if fileobj is None: + handle = sys.stdout + else: + handle = fileobj + + r_cutoff = self.r_cutoff + sf_number = self.symfunc_type_numbers[self.symfunc_type] + + if self.symfunc_type == 'radial': + for comb in self.element_combinations: + for (eta, rs) in zip(self._eta_grid, self._r_shift_grid): + handle.write( + f'symfunction_short {comb[0]:2s} {sf_number} ' + f'{comb[1]:2s} {eta:9.3E} {rs:9.3E} {r_cutoff:9.3E}\n') + handle.write('\n') + + elif self.symfunc_type in ['angular_narrow', 'angular_wide']: + for comb in self.element_combinations: + for (eta, rs) in zip(self._eta_grid, self._r_shift_grid): + for zeta in self.zetas: + for lambd in self.lambdas: + handle.write( + f'symfunction_short {comb[0]:2s} {sf_number} ' + f'{comb[1]:2s} {comb[2]:2s} {eta:9.3E} ' + f'{lambd:2.0f} {zeta:9.3E} {r_cutoff:9.3E} ' + f'{rs:9.3E}\n') + handle.write('\n') + + elif self.symfunc_type == 'weighted_radial': + for comb in self.element_combinations: + for (eta, rs) in zip(self._eta_grid, self._r_shift_grid): + handle.write( + f'symfunction_short {comb[0]:2s} {sf_number} ' + f'{eta:9.3E} {rs:9.3E} {r_cutoff:9.3E}\n') + handle.write('\n') + + elif self.symfunc_type == 'weighted_angular': + for comb in self.element_combinations: + for (eta, rs) in zip(self._eta_grid, self._r_shift_grid): + for zeta in self.zetas: + for lambd in self.lambdas: + handle.write( + f'symfunction_short {comb[0]:2s} {sf_number} ' + f'{eta:9.3E} {rs:9.3E} {lambd:2.0f} ' + f'{zeta:9.3E} {r_cutoff:9.3E} \n') + handle.write('\n') diff --git a/tools/python/symfunc_paramgen/tests/reference-output_write_parameter_strings.txt b/tools/python/symfunc_paramgen/tests/reference-output_write_parameter_strings.txt new file mode 100644 index 000000000..6ca29419e --- /dev/null +++ b/tools/python/symfunc_paramgen/tests/reference-output_write_parameter_strings.txt @@ -0,0 +1,202 @@ +# -------------------------------------------------------------------------------------- +# | Comment lines in this file (lines starting with '#') are not relevant to the tests | +# | and are ignored by them. | +# | They are only a reminder of what settings to use in the tests to recreate the | +# | reference output contained in this file. | +# -------------------------------------------------------------------------------------- +# +######################################################################### +# Radial symmetry function set, for elements ['S', 'Cu'] +######################################################################### +# r_cutoff = 11.22 +# A custom set of values was used for the radial parameters r_shift and eta. +# Thus, there are no settings on radial parameter generation available for display. +# Sets of values for parameters: +# r_shift_grid = [1 2] +# eta_grid = [4 5] +symfunction_short S 2 S 4.000E+00 1.000E+00 1.122E+01 +symfunction_short S 2 S 5.000E+00 2.000E+00 1.122E+01 + +symfunction_short S 2 Cu 4.000E+00 1.000E+00 1.122E+01 +symfunction_short S 2 Cu 5.000E+00 2.000E+00 1.122E+01 + +symfunction_short Cu 2 S 4.000E+00 1.000E+00 1.122E+01 +symfunction_short Cu 2 S 5.000E+00 2.000E+00 1.122E+01 + +symfunction_short Cu 2 Cu 4.000E+00 1.000E+00 1.122E+01 +symfunction_short Cu 2 Cu 5.000E+00 2.000E+00 1.122E+01 + +######################################################################### +# Weighted radial symmetry function set, for elements ['S', 'Cu'] +######################################################################### +# r_cutoff = 11.22 +# A custom set of values was used for the radial parameters r_shift and eta. +# Thus, there are no settings on radial parameter generation available for display. +# Sets of values for parameters: +# r_shift_grid = [1 2] +# eta_grid = [4 5] +symfunction_short S 12 4.000E+00 1.000E+00 1.122E+01 +symfunction_short S 12 5.000E+00 2.000E+00 1.122E+01 + +symfunction_short Cu 12 4.000E+00 1.000E+00 1.122E+01 +symfunction_short Cu 12 5.000E+00 2.000E+00 1.122E+01 + +######################################################################### +# Narrow angular symmetry function set, for elements ['S', 'Cu'] +######################################################################### +# r_cutoff = 11.22 +# A custom set of values was used for the radial parameters r_shift and eta. +# Thus, there are no settings on radial parameter generation available for display. +# Sets of values for parameters: +# r_shift_grid = [1 2] +# eta_grid = [4 5] +# lambdas = [-1. 1.] +# zetas = [5.5 7.5] +symfunction_short S 3 S S 4.000E+00 -1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short S 3 S S 4.000E+00 1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short S 3 S S 4.000E+00 -1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short S 3 S S 4.000E+00 1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short S 3 S S 5.000E+00 -1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short S 3 S S 5.000E+00 1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short S 3 S S 5.000E+00 -1 7.500E+00 1.122E+01 2.000E+00 +symfunction_short S 3 S S 5.000E+00 1 7.500E+00 1.122E+01 2.000E+00 + +symfunction_short S 3 S Cu 4.000E+00 -1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short S 3 S Cu 4.000E+00 1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short S 3 S Cu 4.000E+00 -1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short S 3 S Cu 4.000E+00 1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short S 3 S Cu 5.000E+00 -1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short S 3 S Cu 5.000E+00 1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short S 3 S Cu 5.000E+00 -1 7.500E+00 1.122E+01 2.000E+00 +symfunction_short S 3 S Cu 5.000E+00 1 7.500E+00 1.122E+01 2.000E+00 + +symfunction_short S 3 Cu Cu 4.000E+00 -1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short S 3 Cu Cu 4.000E+00 1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short S 3 Cu Cu 4.000E+00 -1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short S 3 Cu Cu 4.000E+00 1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short S 3 Cu Cu 5.000E+00 -1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short S 3 Cu Cu 5.000E+00 1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short S 3 Cu Cu 5.000E+00 -1 7.500E+00 1.122E+01 2.000E+00 +symfunction_short S 3 Cu Cu 5.000E+00 1 7.500E+00 1.122E+01 2.000E+00 + +symfunction_short Cu 3 S S 4.000E+00 -1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 3 S S 4.000E+00 1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 3 S S 4.000E+00 -1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 3 S S 4.000E+00 1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 3 S S 5.000E+00 -1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 3 S S 5.000E+00 1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 3 S S 5.000E+00 -1 7.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 3 S S 5.000E+00 1 7.500E+00 1.122E+01 2.000E+00 + +symfunction_short Cu 3 S Cu 4.000E+00 -1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 3 S Cu 4.000E+00 1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 3 S Cu 4.000E+00 -1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 3 S Cu 4.000E+00 1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 3 S Cu 5.000E+00 -1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 3 S Cu 5.000E+00 1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 3 S Cu 5.000E+00 -1 7.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 3 S Cu 5.000E+00 1 7.500E+00 1.122E+01 2.000E+00 + +symfunction_short Cu 3 Cu Cu 4.000E+00 -1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 3 Cu Cu 4.000E+00 1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 3 Cu Cu 4.000E+00 -1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 3 Cu Cu 4.000E+00 1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 3 Cu Cu 5.000E+00 -1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 3 Cu Cu 5.000E+00 1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 3 Cu Cu 5.000E+00 -1 7.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 3 Cu Cu 5.000E+00 1 7.500E+00 1.122E+01 2.000E+00 + +######################################################################### +# Wide angular symmetry function set, for elements ['S', 'Cu'] +######################################################################### +# r_cutoff = 11.22 +# A custom set of values was used for the radial parameters r_shift and eta. +# Thus, there are no settings on radial parameter generation available for display. +# Sets of values for parameters: +# r_shift_grid = [1 2] +# eta_grid = [4 5] +# lambdas = [-1. 1.] +# zetas = [5.5 7.5] +symfunction_short S 9 S S 4.000E+00 -1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short S 9 S S 4.000E+00 1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short S 9 S S 4.000E+00 -1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short S 9 S S 4.000E+00 1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short S 9 S S 5.000E+00 -1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short S 9 S S 5.000E+00 1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short S 9 S S 5.000E+00 -1 7.500E+00 1.122E+01 2.000E+00 +symfunction_short S 9 S S 5.000E+00 1 7.500E+00 1.122E+01 2.000E+00 + +symfunction_short S 9 S Cu 4.000E+00 -1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short S 9 S Cu 4.000E+00 1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short S 9 S Cu 4.000E+00 -1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short S 9 S Cu 4.000E+00 1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short S 9 S Cu 5.000E+00 -1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short S 9 S Cu 5.000E+00 1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short S 9 S Cu 5.000E+00 -1 7.500E+00 1.122E+01 2.000E+00 +symfunction_short S 9 S Cu 5.000E+00 1 7.500E+00 1.122E+01 2.000E+00 + +symfunction_short S 9 Cu Cu 4.000E+00 -1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short S 9 Cu Cu 4.000E+00 1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short S 9 Cu Cu 4.000E+00 -1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short S 9 Cu Cu 4.000E+00 1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short S 9 Cu Cu 5.000E+00 -1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short S 9 Cu Cu 5.000E+00 1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short S 9 Cu Cu 5.000E+00 -1 7.500E+00 1.122E+01 2.000E+00 +symfunction_short S 9 Cu Cu 5.000E+00 1 7.500E+00 1.122E+01 2.000E+00 + +symfunction_short Cu 9 S S 4.000E+00 -1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 9 S S 4.000E+00 1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 9 S S 4.000E+00 -1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 9 S S 4.000E+00 1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 9 S S 5.000E+00 -1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 9 S S 5.000E+00 1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 9 S S 5.000E+00 -1 7.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 9 S S 5.000E+00 1 7.500E+00 1.122E+01 2.000E+00 + +symfunction_short Cu 9 S Cu 4.000E+00 -1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 9 S Cu 4.000E+00 1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 9 S Cu 4.000E+00 -1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 9 S Cu 4.000E+00 1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 9 S Cu 5.000E+00 -1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 9 S Cu 5.000E+00 1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 9 S Cu 5.000E+00 -1 7.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 9 S Cu 5.000E+00 1 7.500E+00 1.122E+01 2.000E+00 + +symfunction_short Cu 9 Cu Cu 4.000E+00 -1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 9 Cu Cu 4.000E+00 1 5.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 9 Cu Cu 4.000E+00 -1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 9 Cu Cu 4.000E+00 1 7.500E+00 1.122E+01 1.000E+00 +symfunction_short Cu 9 Cu Cu 5.000E+00 -1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 9 Cu Cu 5.000E+00 1 5.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 9 Cu Cu 5.000E+00 -1 7.500E+00 1.122E+01 2.000E+00 +symfunction_short Cu 9 Cu Cu 5.000E+00 1 7.500E+00 1.122E+01 2.000E+00 + +######################################################################### +# Weighted angular symmetry function set, for elements ['S', 'Cu'] +######################################################################### +# r_cutoff = 11.22 +# A custom set of values was used for the radial parameters r_shift and eta. +# Thus, there are no settings on radial parameter generation available for display. +# Sets of values for parameters: +# r_shift_grid = [1 2] +# eta_grid = [4 5] +# lambdas = [-1. 1.] +# zetas = [5.5 7.5] +symfunction_short S 13 4.000E+00 1.000E+00 -1 5.500E+00 1.122E+01 +symfunction_short S 13 4.000E+00 1.000E+00 1 5.500E+00 1.122E+01 +symfunction_short S 13 4.000E+00 1.000E+00 -1 7.500E+00 1.122E+01 +symfunction_short S 13 4.000E+00 1.000E+00 1 7.500E+00 1.122E+01 +symfunction_short S 13 5.000E+00 2.000E+00 -1 5.500E+00 1.122E+01 +symfunction_short S 13 5.000E+00 2.000E+00 1 5.500E+00 1.122E+01 +symfunction_short S 13 5.000E+00 2.000E+00 -1 7.500E+00 1.122E+01 +symfunction_short S 13 5.000E+00 2.000E+00 1 7.500E+00 1.122E+01 + +symfunction_short Cu 13 4.000E+00 1.000E+00 -1 5.500E+00 1.122E+01 +symfunction_short Cu 13 4.000E+00 1.000E+00 1 5.500E+00 1.122E+01 +symfunction_short Cu 13 4.000E+00 1.000E+00 -1 7.500E+00 1.122E+01 +symfunction_short Cu 13 4.000E+00 1.000E+00 1 7.500E+00 1.122E+01 +symfunction_short Cu 13 5.000E+00 2.000E+00 -1 5.500E+00 1.122E+01 +symfunction_short Cu 13 5.000E+00 2.000E+00 1 5.500E+00 1.122E+01 +symfunction_short Cu 13 5.000E+00 2.000E+00 -1 7.500E+00 1.122E+01 +symfunction_short Cu 13 5.000E+00 2.000E+00 1 7.500E+00 1.122E+01 + diff --git a/tools/python/symfunc_paramgen/tests/reference-output_write_settings_overview.txt b/tools/python/symfunc_paramgen/tests/reference-output_write_settings_overview.txt new file mode 100644 index 000000000..5ea20e31b --- /dev/null +++ b/tools/python/symfunc_paramgen/tests/reference-output_write_settings_overview.txt @@ -0,0 +1,137 @@ +######################################################################### +# Radial symmetry function set, for elements ['S', 'Cu'] +######################################################################### +# r_cutoff = 11.22 +# A custom set of values was used for the radial parameters r_shift and eta. +# Thus, there are no settings on radial parameter generation available for display. +# Sets of values for parameters: +# r_shift_grid = [1 2] +# eta_grid = [4 5] + +######################################################################### +# Weighted radial symmetry function set, for elements ['S', 'Cu'] +######################################################################### +# r_cutoff = 11.22 +# A custom set of values was used for the radial parameters r_shift and eta. +# Thus, there are no settings on radial parameter generation available for display. +# Sets of values for parameters: +# r_shift_grid = [1 2] +# eta_grid = [4 5] + +######################################################################### +# Narrow angular symmetry function set, for elements ['S', 'Cu'] +######################################################################### +# r_cutoff = 11.22 +# A custom set of values was used for the radial parameters r_shift and eta. +# Thus, there are no settings on radial parameter generation available for display. +# Sets of values for parameters: +# r_shift_grid = [1 2] +# eta_grid = [4 5] +# lambdas = [-1. 1.] +# zetas = [5.5 7.5] + +######################################################################### +# Wide angular symmetry function set, for elements ['S', 'Cu'] +######################################################################### +# r_cutoff = 11.22 +# A custom set of values was used for the radial parameters r_shift and eta. +# Thus, there are no settings on radial parameter generation available for display. +# Sets of values for parameters: +# r_shift_grid = [1 2] +# eta_grid = [4 5] +# lambdas = [-1. 1.] +# zetas = [5.5 7.5] + +######################################################################### +# Weighted angular symmetry function set, for elements ['S', 'Cu'] +######################################################################### +# r_cutoff = 11.22 +# A custom set of values was used for the radial parameters r_shift and eta. +# Thus, there are no settings on radial parameter generation available for display. +# Sets of values for parameters: +# r_shift_grid = [1 2] +# eta_grid = [4 5] +# lambdas = [-1. 1.] +# zetas = [5.5 7.5] + +######################################################################### +# Radial symmetry function set, for elements ['S', 'Cu'] +######################################################################### +# r_cutoff = 11.22 +# The following settings were used for generating sets +# of values for the radial parameters r_shift and eta: +# rule = gastegger2018 +# mode = center +# nb_param_pairs = 2 +# r_lower = 1.5 +# r_upper = 9.0 +# Sets of values for parameters: +# r_shift_grid = [0. 0.] +# eta_grid = [0.2222 0.0062] + +######################################################################### +# Weighted radial symmetry function set, for elements ['S', 'Cu'] +######################################################################### +# r_cutoff = 11.22 +# The following settings were used for generating sets +# of values for the radial parameters r_shift and eta: +# rule = gastegger2018 +# mode = center +# nb_param_pairs = 2 +# r_lower = 1.5 +# r_upper = 9.0 +# Sets of values for parameters: +# r_shift_grid = [0. 0.] +# eta_grid = [0.2222 0.0062] + +######################################################################### +# Narrow angular symmetry function set, for elements ['S', 'Cu'] +######################################################################### +# r_cutoff = 11.22 +# The following settings were used for generating sets +# of values for the radial parameters r_shift and eta: +# rule = gastegger2018 +# mode = center +# nb_param_pairs = 2 +# r_lower = 1.5 +# r_upper = 9.0 +# Sets of values for parameters: +# r_shift_grid = [0. 0.] +# eta_grid = [0.2222 0.0062] +# lambdas = [-1. 1.] +# zetas = [5.5 7.5] + +######################################################################### +# Wide angular symmetry function set, for elements ['S', 'Cu'] +######################################################################### +# r_cutoff = 11.22 +# The following settings were used for generating sets +# of values for the radial parameters r_shift and eta: +# rule = gastegger2018 +# mode = center +# nb_param_pairs = 2 +# r_lower = 1.5 +# r_upper = 9.0 +# Sets of values for parameters: +# r_shift_grid = [0. 0.] +# eta_grid = [0.2222 0.0062] +# lambdas = [-1. 1.] +# zetas = [5.5 7.5] + +######################################################################### +# Weighted angular symmetry function set, for elements ['S', 'Cu'] +######################################################################### +# r_cutoff = 11.22 +# The following settings were used for generating sets +# of values for the radial parameters r_shift and eta: +# rule = gastegger2018 +# mode = center +# nb_param_pairs = 2 +# r_lower = 1.5 +# r_upper = 9.0 +# Sets of values for parameters: +# r_shift_grid = [0. 0.] +# eta_grid = [0.2222 0.0062] +# lambdas = [-1. 1.] +# zetas = [5.5 7.5] + diff --git a/tools/python/symfunc_paramgen/tests/test_generate_radial_params.py b/tools/python/symfunc_paramgen/tests/test_generate_radial_params.py new file mode 100644 index 000000000..e3cdaf865 --- /dev/null +++ b/tools/python/symfunc_paramgen/tests/test_generate_radial_params.py @@ -0,0 +1,176 @@ +import sys +sys.path.append('../src') + +import numpy as np +import pytest +import os +from sfparamgen import SymFuncParamGenerator + + +@pytest.fixture +def basic_generator(): + elems = ['S', 'Cu'] + return SymFuncParamGenerator(elements=elems, r_cutoff=6.) + + +@pytest.mark.parametrize("rule,mode,nb_param_pairs,r_lower", [ + ('bad_argument', 'center', 5, 1.5), + ('bad_argument', 'shift', 5, 1.5), + ('gastegger2018', 'bad_argument', 5, 1.5), + ('imbalzano2018', 'bad_argument', 5, None) +]) +def test_errors_rule_mode(basic_generator, + rule, mode, nb_param_pairs, r_lower): + """Test for ValueError when invalid arguments for rule or method. + """ + with pytest.raises(ValueError): + basic_generator.generate_radial_params(rule=rule, mode=mode, + nb_param_pairs=nb_param_pairs, r_lower=r_lower) + + +@pytest.mark.parametrize("rule,mode,nb_param_pairs", [ + ('gastegger2018', 'center', 5), + ('gastegger2018', 'shift', 5) +]) +def test_errors_rlower(basic_generator, + rule, mode, nb_param_pairs): + """Test for TypeError when omitting r_lower argument in rule 'gastegger2018' + """ + with pytest.raises(TypeError): + basic_generator.generate_radial_params(rule=rule, mode=mode, + nb_param_pairs=nb_param_pairs) + + +@pytest.mark.parametrize("rule,mode,nb_param_pairs,r_lower", [ + ('gastegger2018', 'center', 1, 1.5), + ('gastegger2018', 'shift', 1, 1.5), + ('imbalzano2018', 'center', 1, None), + ('imbalzano2018', 'shift', 1, None) +]) +def test_errors_nb_param_pairs(basic_generator, + rule, mode, nb_param_pairs, r_lower): + """Test for ValueError when nb_param_pairs less than two. + """ + with pytest.raises(ValueError): + basic_generator.generate_radial_params(rule=rule, mode=mode, + nb_param_pairs=nb_param_pairs, r_lower=r_lower) + + +@pytest.mark.parametrize("rule,mode,nb_param_pairs,r_lower,r_upper", [ + ('gastegger2018', 'center', 2, 0., 5.), + ('gastegger2018', 'center', 2, 0., None), + ('gastegger2018', 'center', 2, 5., 1.), + ('gastegger2018', 'center', 2, 7., None), + ('gastegger2018', 'center', 2, 1., 7.), + ('gastegger2018', 'shift', 2, -1., 5.), + ('gastegger2018', 'shift', 2, -1., None), + ('gastegger2018', 'shift', 2, 5., 1.), + ('gastegger2018', 'shift', 2, 7, None), + ('gastegger2018', 'shift', 2, 1., 7.) +]) +def test_errors_numerical_order(basic_generator, + rule, mode, nb_param_pairs, r_lower, r_upper): + """Test for ValueError when illegal relation between r_lower, r_upper, r_cutoff in rule 'gastegger2018' + """ + with pytest.raises(ValueError): + basic_generator.generate_radial_params(rule=rule, mode=mode, + nb_param_pairs=nb_param_pairs, + r_lower=r_lower, r_upper=r_upper) + +@pytest.mark.parametrize("rule,mode,nb_param_pairs,r_lower,r_upper,target_r_shift,target_eta", [ + ('gastegger2018', 'center', 3, 1., 5., + [0, 0, 0], + [1/2, 1/18, 1/50]), + ('gastegger2018', 'shift', 3, 1., 5., + [1, 3, 5], + [1/8, 1/8, 1/8]), + ('gastegger2018', 'center', 3, 1., None, + [0, 0, 0], + [1/2, 1/(2*3.5**2), 1/(2*6**2)]), + ('gastegger2018', 'shift', 3, 1., None, + [1, 3.5, 6], + [1/(2*2.5**2)]*3) +]) +def test_parameter_generation_gastegger2018(basic_generator, + rule, mode, nb_param_pairs, r_lower, r_upper, + target_r_shift, target_eta): + """Test if generated r_shift and eta values match target values. + """ + # when passing r_upper + if r_upper is not None: + basic_generator.generate_radial_params(rule=rule, mode=mode, + nb_param_pairs=nb_param_pairs, + r_lower=r_lower, r_upper=r_upper) + # when not passing r_upper, using default value of r_cutoff + else: + basic_generator.generate_radial_params(rule=rule, mode=mode, + nb_param_pairs=nb_param_pairs, + r_lower=r_lower) + + # test if generated parameter sets match target values + assert np.allclose(basic_generator.r_shift_grid, np.array(target_r_shift)) + assert np.allclose(basic_generator.eta_grid, np.array(target_eta)) + + # test if settings have been correctly stored + assert basic_generator.radial_paramgen_settings['rule'] == rule + assert basic_generator.radial_paramgen_settings['mode'] == mode + assert basic_generator.radial_paramgen_settings['nb_param_pairs'] == nb_param_pairs + assert basic_generator.radial_paramgen_settings['r_lower'] == r_lower + if r_upper is not None: + assert basic_generator.radial_paramgen_settings['r_upper'] == r_upper + else: + assert basic_generator.radial_paramgen_settings['r_upper'] == basic_generator.r_cutoff + + +@pytest.mark.parametrize("rule,mode,nb_param_pairs,target_r_shift,target_eta", [ + ('imbalzano2018', 'center', 4, + [0, 0, 0, 0], + [1/36, 0.0577801, 0.1201874, 1/4]), + ('imbalzano2018', 'shift', 4, + [1.5, 2.1213203, 3.0, 4.2426407], + [2.5904121, 1.2952060, 0.6476030, 0.3238015]) +]) +def test_parameter_generation_imbalzano2018(basic_generator, + rule, mode, nb_param_pairs, + target_r_shift, target_eta): + """Test if generated r_shift and eta values match target values. + """ + basic_generator.generate_radial_params(rule=rule, mode=mode, + nb_param_pairs=nb_param_pairs) + + # test if generated parameter sets match target values + assert np.allclose(basic_generator.r_shift_grid, np.array(target_r_shift)) + assert np.allclose(basic_generator.eta_grid, np.array(target_eta)) + + # test if settings have been correctly stored + assert basic_generator.radial_paramgen_settings['rule'] == rule + assert basic_generator.radial_paramgen_settings['mode'] == mode + assert basic_generator.radial_paramgen_settings['nb_param_pairs'] == nb_param_pairs + + +@pytest.mark.parametrize("mode", ['center', 'shift']) +def test_warning_unused_args(basic_generator, mode): + """Test if warning when passing r_lower or r_upper while using rule 'imbalzano2018' + """ + with pytest.warns(UserWarning): + basic_generator.generate_radial_params(rule='imbalzano2018', mode=mode, + nb_param_pairs=3, + r_lower=1) + with pytest.warns(UserWarning): + basic_generator.generate_radial_params(rule='imbalzano2018', mode=mode, + nb_param_pairs=3, + r_upper=5) + with pytest.warns(UserWarning): + basic_generator.generate_radial_params(rule='imbalzano2018', mode=mode, + nb_param_pairs=3, + r_lower=1, r_upper=5) + + + + + + + + + + diff --git a/tools/python/symfunc_paramgen/tests/test_misc.py b/tools/python/symfunc_paramgen/tests/test_misc.py new file mode 100644 index 000000000..f0406c730 --- /dev/null +++ b/tools/python/symfunc_paramgen/tests/test_misc.py @@ -0,0 +1,147 @@ +import sys +sys.path.append('../src') + +import numpy as np +import pytest +import os +from sfparamgen import SymFuncParamGenerator + + +@pytest.fixture +def basic_generator(): + elems = ['S', 'Cu'] + return SymFuncParamGenerator(elements=elems, r_cutoff=6.) + + +@pytest.mark.parametrize("symfunc_type,target_combinations", [ + ('radial', + [('H', 'H'), ('H', 'C'), ('H', 'O'), + ('C', 'H'), ('C', 'C'), ('C', 'O'), + ('O', 'H'), ('O', 'C'), ('O', 'O')]), + ('angular_narrow', + [('H', 'H', 'H'), ('H', 'H', 'C'), ('H', 'H', 'O'), ('H', 'C', 'C'), + ('H', 'C', 'O'), ('H', 'O', 'O'), + ('C', 'H', 'H'), ('C', 'H', 'C'), ('C', 'H', 'O'), ('C', 'C', 'C'), + ('C', 'C', 'O'), ('C', 'O', 'O'), + ('O', 'H', 'H'), ('O', 'H', 'C'), ('O', 'H', 'O'), ('O', 'C', 'C'), + ('O', 'C', 'O'), ('O', 'O', 'O')]), + ('angular_wide', + [('H', 'H', 'H'), ('H', 'H', 'C'), ('H', 'H', 'O'), ('H', 'C', 'C'), + ('H', 'C', 'O'), ('H', 'O', 'O'), + ('C', 'H', 'H'), ('C', 'H', 'C'), ('C', 'H', 'O'), ('C', 'C', 'C'), + ('C', 'C', 'O'), ('C', 'O', 'O'), + ('O', 'H', 'H'), ('O', 'H', 'C'), ('O', 'H', 'O'), ('O', 'C', 'C'), + ('O', 'C', 'O'), ('O', 'O', 'O')]), + ('weighted_radial', + [('H',), ('C',), ('O',)]), + ('weighted_angular', + [('H',), ('C',), ('O',)]) +]) +def test_element_combinations(symfunc_type, target_combinations): + """Test if element combinations are correctly constructed. + """ + elems = ['H', 'C', 'O'] + myGen = SymFuncParamGenerator(elements=elems, r_cutoff=6.) + myGen.symfunc_type = symfunc_type + assert myGen.element_combinations == target_combinations + + +def test_rcutoff(): + elems = ['S', 'Cu'] + # test for errors when cutoff radius not greater than zero + with pytest.raises(ValueError): + myGen = SymFuncParamGenerator(elements=elems, r_cutoff=0) + with pytest.raises(ValueError): + myGen = SymFuncParamGenerator(elements=elems, r_cutoff=-5) + # test if initializing and retrieving r_cutoff works as expected + myGen = SymFuncParamGenerator(elements=elems, r_cutoff=6) + assert myGen.r_cutoff == 6 + # test for AttributeError when trying to change r_cutoff afterwards + with pytest.raises(AttributeError): + myGen.r_cutoff = 10 + + +def test_setter_symfunc_type(basic_generator): + """Test if error when trying to set invalid symmetry function type. + """ + with pytest.raises(ValueError): + basic_generator.symfunc_type = 'illegal_type' + + +@pytest.mark.parametrize("symfunc_type,r_shift_grid,eta_grid,zetas", [ + (None, np.array([1,2,3]), np.array([4,5,6]), None), + ('radial', None, np.array([4,5,6]), None), + ('radial', np.array([1,2,3]), None, None), + ('weighted_radial', None, np.array([4, 5, 6]), None), + ('weighted_radial', np.array([1, 2, 3]), None, None), + ('angular_narrow', None, np.array([4,5,6]), np.array([1,4,9])), + ('angular_narrow', np.array([1,2,3]), None, np.array([1,4,9])), + ('angular_narrow', np.array([1,2,3]), np.array([4,5,6]), None), + ('angular_wide', None, np.array([4,5,6]), np.array([1,4,9])), + ('angular_wide', np.array([1,2,3]), None, np.array([1,4,9])), + ('angular_wide', np.array([1,2,3]), np.array([4,5,6]), None), + ('weighted_angular', None, np.array([4,5,6]), np.array([1,4,9])), + ('weighted_angular', np.array([1,2,3]), None, np.array([1,4,9])), + ('weighted_angular', np.array([1,2,3]), np.array([4,5,6]), None) +]) +def test_check_writing_prerequisites(basic_generator, + symfunc_type, r_shift_grid, eta_grid, zetas): + """Test if error when writing parameters or settings while data are missing. + """ + if symfunc_type is not None: + basic_generator.symfunc_type = symfunc_type + if r_shift_grid is not None: + basic_generator._r_shift_grid = r_shift_grid + if eta_grid is not None: + basic_generator._eta_grid = eta_grid + if zetas is not None: + basic_generator.zetas = zetas + + # test the completeness checker on its own (without argument) + with pytest.raises(ValueError): + basic_generator.check_writing_prerequisites() + # test the methods that call the completeness checker (which call it with argument) + with pytest.raises(ValueError): + basic_generator.write_settings_overview() + with pytest.raises(ValueError): + basic_generator.write_parameter_strings() + + +def test_set_custom_radial_params(basic_generator): + """Test if set_custom_radial_params correctly raises exceptions and sets values. + """ + # test for exception when unequal length + with pytest.raises(TypeError): + basic_generator.set_custom_radial_params([1.1, 2.2, 3.3], [4.4, 5.5]) + # test for exception when negative value in values for r_shift + with pytest.raises(ValueError): + basic_generator.set_custom_radial_params([-0.1, 2.2, 3.3], [1.1, 2.2, 3.3]) + # test for exception when non-positive value in values for eta + with pytest.raises(ValueError): + basic_generator.set_custom_radial_params([1.1, 2.2, 3.3], [0, 2.2, 3.3]) + + # test if setting custom r_shift and eta values works correctly + basic_generator.set_custom_radial_params([1.1, 2.2, 3.3], [3.3, 2.2, 1.1]) + assert np.array_equal(basic_generator.r_shift_grid, np.array([1.1, 2.2, 3.3])) + assert np.array_equal(basic_generator.eta_grid, np.array([3.3, 2.2, 1.1])) + + # test if the dict containing radial parameter generation settings is + # correctly reset to None, when setting custom radial parameters + basic_generator.generate_radial_params(rule='imbalzano2018', mode='center', nb_param_pairs=3) + basic_generator.set_custom_radial_params([1.1, 2.2, 3.3], [3.3, 2.2, 1.1]) + assert basic_generator.radial_paramgen_settings is None + + + + + + + + + + + + + + + diff --git a/tools/python/symfunc_paramgen/tests/test_write_parameter_strings.py b/tools/python/symfunc_paramgen/tests/test_write_parameter_strings.py new file mode 100644 index 000000000..757bb4c73 --- /dev/null +++ b/tools/python/symfunc_paramgen/tests/test_write_parameter_strings.py @@ -0,0 +1,108 @@ +import sys +sys.path.append('../src') + +import numpy as np +import pytest +import os +import sys +from sfparamgen import SymFuncParamGenerator + + +REFERENCE_PATH = 'reference-output_write_parameter_strings.txt' + + +@pytest.fixture +def basic_generator(): + elems = ['S', 'Cu'] + return SymFuncParamGenerator(elements=elems, r_cutoff=11.22) + + +def isnotcomment(line): + """Filter lines according to whether they start in '#'. + """ + if line[0] == '#': + return False + return True + + +def test_write_parameter_strings_file(basic_generator, tmpdir): + '''Test parameter strings written to file match reference, for toy inputs. + ''' + outfile_path = os.path.join(tmpdir, 'outfile.txt') + + basic_generator.set_custom_radial_params([1,2], [4,5]) + + with open(outfile_path, 'w') as outfile_obj: + ## radial + basic_generator.symfunc_type = 'radial' + basic_generator.write_parameter_strings(outfile_obj) + + ## weighted_radial + basic_generator.symfunc_type = 'weighted_radial' + basic_generator.write_parameter_strings(outfile_obj) + + ## angular narrow + basic_generator.symfunc_type = 'angular_narrow' + basic_generator.zetas = [5.5, 7.5] + basic_generator.write_parameter_strings(outfile_obj) + + ## angular_wide + basic_generator.symfunc_type = 'angular_wide' + basic_generator.zetas = [5.5, 7.5] + basic_generator.write_parameter_strings(outfile_obj) + + ## weighted_angular + basic_generator.symfunc_type = 'weighted_angular' + basic_generator.zetas = [5.5, 7.5] + basic_generator.write_parameter_strings(outfile_obj) + + # Ignore comment lines in the reference output file (lines starting + # with '#'). These lines are included in the reference output file as a + # reminder of what settings to use in the tests to recreate the reference + # output, but they are otherwise not essential for the test. + with open(outfile_path) as f_out, open(REFERENCE_PATH) as f_reference: + f_reference = filter(isnotcomment, f_reference) + assert all(x == y for x, y in zip(f_out, f_reference)) + + +def test_write_parameter_strings_stdout(basic_generator, capsys): + '''Test parameter strings written to stdout match reference, for toy inputs. + ''' + basic_generator.set_custom_radial_params([1,2], [4,5]) + + ## radial + basic_generator.symfunc_type = 'radial' + basic_generator.write_parameter_strings() + + ## weighted_radial + basic_generator.symfunc_type = 'weighted_radial' + basic_generator.write_parameter_strings() + + ## angular narrow + basic_generator.symfunc_type = 'angular_narrow' + basic_generator.zetas = [5.5, 7.5] + basic_generator.write_parameter_strings() + + ## angular_wide + basic_generator.symfunc_type = 'angular_wide' + basic_generator.zetas = [5.5, 7.5] + basic_generator.write_parameter_strings() + + ## weighted_angular + basic_generator.symfunc_type = 'weighted_angular' + basic_generator.zetas = [5.5, 7.5] + basic_generator.write_parameter_strings() + + # capture the output written to stdout by parameter string writing method + captured = capsys.readouterr() + + # compare what was written to stdout with the reference output file + reference_data = [] + with open(REFERENCE_PATH, 'r') as f_reference: + for line in f_reference: + # ignore comment lines in reference output file + if not line[0] == '#': + reference_data.append(line) + assert ''.join(reference_data) == captured.out + + diff --git a/tools/python/symfunc_paramgen/tests/test_write_settings_overview.py b/tools/python/symfunc_paramgen/tests/test_write_settings_overview.py new file mode 100644 index 000000000..1682afdf9 --- /dev/null +++ b/tools/python/symfunc_paramgen/tests/test_write_settings_overview.py @@ -0,0 +1,140 @@ +import sys +sys.path.append('../src') + +import numpy as np +import pytest +import os +from sfparamgen import SymFuncParamGenerator +import filecmp + + +REFERENCE_PATH = 'reference-output_write_settings_overview.txt' + + +@pytest.fixture +def basic_generator(): + elems = ['S', 'Cu'] + return SymFuncParamGenerator(elements=elems, r_cutoff=11.22) + + +def test_write_settings_overview_file(basic_generator, tmpdir): + outfile_path = os.path.join(tmpdir, 'outfile.txt') + + ########### using custom radial parameters ########### + basic_generator.set_custom_radial_params(r_shift_values=[1,2], + eta_values=[4,5]) + + with open(outfile_path, 'w') as outfile_obj: + ## radial + basic_generator.symfunc_type = 'radial' + basic_generator.write_settings_overview(outfile_obj) + + ## weighted_radial + basic_generator.symfunc_type = 'weighted_radial' + basic_generator.write_settings_overview(outfile_obj) + + basic_generator.zetas = [5.5, 7.5] + + ## angular narrow + basic_generator.symfunc_type = 'angular_narrow' + basic_generator.write_settings_overview(outfile_obj) + + ## angular_wide + basic_generator.symfunc_type = 'angular_wide' + basic_generator.write_settings_overview(outfile_obj) + + ## weighted_angular + basic_generator.symfunc_type = 'weighted_angular' + basic_generator.write_settings_overview(outfile_obj) + + ########### using the method for radial parameter generation ########### + basic_generator.generate_radial_params(rule='gastegger2018', mode='center', + nb_param_pairs=2, r_lower=1.5, r_upper=9.) + + ## radial + basic_generator.symfunc_type = 'radial' + basic_generator.write_settings_overview(outfile_obj) + + ## weighted_radial + basic_generator.symfunc_type = 'weighted_radial' + basic_generator.write_settings_overview(outfile_obj) + + basic_generator.zetas = [5.5, 7.5] + + ## angular narrow + basic_generator.symfunc_type = 'angular_narrow' + basic_generator.write_settings_overview(outfile_obj) + + ## angular_wide + basic_generator.symfunc_type = 'angular_wide' + basic_generator.write_settings_overview(outfile_obj) + + ## weighted_angular + basic_generator.symfunc_type = 'weighted_angular' + basic_generator.write_settings_overview(outfile_obj) + + ########### test equality with target output ########### + assert filecmp.cmp(outfile_path, REFERENCE_PATH) + + +def test_write_settings_overview_stdout(basic_generator, capsys): + ########### using custom radial parameters ########### + basic_generator.set_custom_radial_params(r_shift_values=[1,2], + eta_values=[4,5]) + + ## radial + basic_generator.symfunc_type = 'radial' + basic_generator.write_settings_overview() + + ## weighted_radial + basic_generator.symfunc_type = 'weighted_radial' + basic_generator.write_settings_overview() + + basic_generator.zetas = [5.5, 7.5] + + ## angular narrow + basic_generator.symfunc_type = 'angular_narrow' + basic_generator.write_settings_overview() + + ## angular_wide + basic_generator.symfunc_type = 'angular_wide' + basic_generator.write_settings_overview() + + ## weighted_angular + basic_generator.symfunc_type = 'weighted_angular' + basic_generator.write_settings_overview() + + ########### using the method for radial parameter generation ########### + basic_generator.generate_radial_params(rule='gastegger2018', mode='center', + nb_param_pairs=2, r_lower=1.5, r_upper=9.) + + ## radial + basic_generator.symfunc_type = 'radial' + basic_generator.write_settings_overview() + + ## weighted_radial + basic_generator.symfunc_type = 'weighted_radial' + basic_generator.write_settings_overview() + + basic_generator.zetas = [5.5, 7.5] + + ## angular narrow + basic_generator.symfunc_type = 'angular_narrow' + basic_generator.write_settings_overview() + + ## angular_wide + basic_generator.symfunc_type = 'angular_wide' + basic_generator.write_settings_overview() + + ## weighted_angular + basic_generator.symfunc_type = 'weighted_angular' + basic_generator.write_settings_overview() + + # capture the output written to stdout by settings writing method + captured = capsys.readouterr() + + ########### test equality with target output ########### + with open(REFERENCE_PATH, 'r') as f_reference: + assert captured.out == f_reference.read() + +