YAML-based tests: design principles and implementation details¶
This document discusses the new YAML-based format used to represent physical results in the main output file. The connection with the ABINIT test suite and the yaml syntax used to define tolerances, parameters and constraints is also discussed.
The new infrastructure consists of a set of Fortran modules to output structured data in YAML format and Python code to parse the output files and analyze data.
Motivations¶
In ABINITv8 and previous versions, the ABINIT test suite is based on input files with the associated reference output files. Roughly speaking, an automatic test consists in comparing the reference file with the output file in a line-oriented fashion by computing differences between floating-point numbers without any knowledge about the meaning and the importance of the numerical values. This approach is very rigid and rather limited because it obliges developers to use a single (usually large) tolerance to account for possibly large fluctuations in the intermediate results whereas the tolerance criteria should be ideally applied only to the final results that are (hopefully) independent of details such as hardware, optimization level and parallelism.
This limitation is clearly seen when comparing the results of iterative algorithms. The number of iterations required to converge, indeed, may depend on several factors especially when the input parameters are far from convergence or when different parallelization schemes or stochastic methods are used. From the point of view of code validation, what really matters is the final converged value plus the time to solution if performance ends up being of concern. Unfortunately, any line-by-line comparison algorithm will miserably fail in such conditions because it will continue to insist on having the same number of iterations (lines) in the two calculations to consider the test succeeded. It is therefore clear that the approach used so far to validate new developments in Abinit is not able to cope with the challenges posed by high-performance computing and that smarter and more flexible approaches are needed to address these limitations. Ideally, we would like to be able to
- use different tolerances for particular quantities that are selected by keyword
- be able to replace the check on the absolute and relative difference with a threshold check (is this quantity smaller that the give threshold?)
- have some sort of syntax to apply different rules depending on the iteration state e.g. the dataset index
- execute python code (callbacks) that operates on the data to perform more advanced tests requiring some sort of post-processing
- provide an easy-to-use declarative interface that allows developers to define the logic to compare selected quantities.
In what follows, we present this new infrastructure, the design principles and the steps required to define YAML-based tests.
Important
The new YAML-based testsuite relies on libraries that are not provided by the python standard library. To install these dependencies in user mode, use:
pip install numpy pyyaml pandas --user
If these dependencies are not available, the new test system will be disabled and a warning message is printed to the terminal.
Implementation details¶
For reasons that will be clear later, implementing smart algorithms requires metadata and context. In other words, the python code needs to have some basic understanding of the meaning of the numerical values extracted from the output file and must be able to locate a particular property by name or by its “position” inside the output file. For this reason, the most important physical results are now written in the main output file (ab_out) using machine-readable YAML documents.
A YAML document starts with three hyphens (—) followed by an
optional tag beginning with an exclamation mark (e.g. !ETOT
).
Three periods (…) signals the end of the document.
Following these rules, one can easily write a dictionary containing the different contributions
to the total free energy using:
--- !EnergyTerms
comment : Components of total free energy (in Hartree)
kinetic : 4.323825067238190201E+01
hartree : 3.133994553363548619E+01
xc : -2.246182631222462689E+01
Ewald energy : -1.142203169790575572E+02
psp_core : 6.536925226004570710E+00
local_psp : -9.852521179991468614E+01
spherical_terms : 2.587718945528139081E+00
internal : -1.515045147136467563E+02
'-kT*entropy' : -3.174881766478041684E-03
total_energy : -1.515076895954132397E+02
total_energy_eV : -4.122733899322517573E+03
...
Further details about the meaning of tags, labels and their connection with the testing infrastructure will be given in the below sections. For the time being, it is sufficient to say that we opted for YAML because it is a human-readable data serialization language already used in the log file to record important events such as WARNINGs, ERRORs and COMMENTs (this is indeed the protocol employed by AbiPy to monitor the status of Abinit calculations). Many programming languages, including python, provide support for YAML hence it is relatively easy to implement post processing tools based on well-established python libraries for scientific computing such as NumPy, SciPy, Matplotlib and Pandas. Last but not least, writing YAML in Fortran does not represent an insurmountable problem provided one keeps the complexity of the YAML document at a reasonable level. Our Fortran implementation, indeed, supports only a subset of the YAML specifications:
- scalars
- arrays with one or two dimensions
- dictionaries mapping strings to scalars
- tables in CSV format (this is an extension of the standard)
Important
YAML is not designed to handle large amount of data therefore it should not be used to represent large arrays for which performance is critical and human-readability is lost by definition (do you really consider a YAML list with one thousand numbers human-readable?). Following this philosophy, YAML is supposed to be used to print the most important results in the main output file and should not be considered as a replacement for binary netcdf files when it comes to storing large data structures with lots of metadata.
Note also that we do not plan to rewrite entirely the main output file in YAML syntax but we prefer to focus on those physical properties that will be used by the new test procedure to validate new developments. This approach, indeed, will facilitate the migration to the new YAML-based approach as only selected portions of the output file will be ported to the new format thus maintaining the look and the feel relatively close to the previous unstructured format.
YAML configuration file¶
How to activate the YAML mode¶
The parameters governing the execution of the test
are specified in the TEST_INFO
section located at the end of the input file.
The options are given in the INI file format.
The integration of the new YAML-based tests with the pre-existent infrastructure
is obtained via two modifications of the current specifications.
More specifically:
-
the files_to_test section now accepts the optional argument use_yaml. The allowed values are:
- “yes” → activate YAML mode
- “no” → do not use YAML mode (default)
- “only” → use YAML mode, deactivate legacy fldiff algorithm
-
a new optional section [yaml_test] has been added. This section contains two mutually exclusive fields:
-
file → path of the YAML configuration file. The path is relative to the input file. A natural choice would be to use the same prefix as the input file e.g. “./t21.yaml” is the configuration file associated to the input file “t21.in”.
-
test → multi-line string with the YAML specifications. This option may be used for really short configurations heavily relying on the default values.
-
An example of TEST_INFO
section that activates the YAML mode can be found in paral[86]:
#%%<BEGIN TEST_INFO>
#%% [setup]
#%% executable = abinit
#%% [files]
#%% psp_files = 23v.paw, 38sr.paw, 8o.paw
#%% [paral_info]
#%% nprocs_to_test = 4
#%% max_nprocs = 4
#%% [NCPU_4]
#%% files_to_test =
#%% t86_MPI4.out, use_yaml = yes, tolnlines = 4, tolabs = 2.0e-2, tolrel = 1.0, fld_options = -easy;
#%% [extra_info]
#%% authors = B. Amadon, T. Cavignac
#%% keywords = DMFT, FAILS_IFMPI
#%% description = DFT+DMFT for SrVO3 using Hubard I code with KGB parallelism
#%% topics = DMFT, parallelism
#%% [yaml_test]
#%% file = ./t86.yaml
#%%<END TEST_INFO>
with the associated YAML configuration file given by:
tol_eq: 1.0e-6 tol_vec: 1.0e-5 ks: # tolvrs 1.0e-7 tol_abs: 5.0e-6 tol_rel: 5.0e-6 ResultsGS!: convergence: ceil: 1.0e-6 cartesian_stress_tensor: tol_vec: 1.0e-7 # EtotSteps: # data: # callback: # method: last_iter # tol_iter: 3 # Etot(hartree): # tol: 1.0e-7 # deltaE(h): # ceil: 2.0e-8 # residm: # ceil: 5.0e-7 # nres2: # ceil: 2.0e-8 dmft: tol_abs: 2.0e-8 tol_rel: 5.0e-9 ResultsGS!: # hard reset result_gs convergence: ceil: 1.0e-6 deltae: ceil: 2.0e-3 res2: ignore: true fermie: tol_abs: 1.0e-7 tol_rel: 1.0e-8 cartesian_stress_tensor: ignore: true EnergyTerms: total_energy_eV: tol_abs: 1.0e-4 tol_rel: 1.0e-8 EnergyTermsDC: tol_abs: 1.0e-7 total_energy_dc_eV: tol_abs: 1.0e-4 tol_rel: 1.0e-8 # EtotSteps: # data: # callback: # method: last_iter # tol_iter: 3 # Etot(hartree): # tol: 1.0e-7 # deltaE(h): # ceil: 2.0e-4 # residm: # ceil: 1.0e-12 # nres2: # ceil: 2.0e-1 filters: ks: dtset: 1 dmft: dtset: 2
Our first example of YAML configuration file¶
Let us start with a minimalistic example in which we compare the components of
the total free energy in the Etot
document with an absolute tolerance of 1.0e-7 Ha.
The YAML configuration file will look like:
Etot:
tol_abs: 1.0e-7
The tol_abs
keyword defines the constraint that will applied to all the children of the Etot
document.
In other words, all the entries in the Etot
dictionary will be compared with
an absolute tolerance of 1.0e-7 and the default value for the relative difference tol_rel
as this
tolerance is not explicitly specified.
There are however cases in which we would like to specify different tolerances for particular entries
instead of relying on the global tolerances.
The Etot
document, for example, contains the total energy in eV in the Total energy (eV)
entry.
To use a different absolute tolerance for this property, we specialize the rule with the syntax:
Etot:
tol_abs: 1.0e-7
Total energy (eV):
tol_abs: 1.0e-5
To change the default value for the relative difference, it is sufficient to specify the constraint outside of the document:
tol_rel: 1.0e-2
Etot:
tol_abs: 1.0e-7
Total energy (eV):
tol_abs: 1.0e-5
Basic concepts¶
In the previous section, we presented a minimal example of configuration file. In the next paragraphs we will discuss in more detail how to implement more advanced test but before proceeding with the examples, we need to introduce some basic terminology to facilitate the discussion.
- Document tree
- The YAML document is a dictionary that can be treated as a tree whose nodes have a label and leaf are scalars or special data structures identified by a tag (note however that not all tags mark a leaf). The top-level nodes are the YAML documents and their labels are the names of their tag.
- Config tree
- The YAML configuration also takes the form of a tree where nodes are specializations and its leaf represent parameters or constraints. Its structure matches the structure of the document tree thus one can define rules (constraint and parameters) that will be applied to a specific part of the document tree.
- Specialization
- The rules defined under a specialization will apply only on the matching node of the document tree and its children.
- Constraint
- A constraint is a condition one imposes for the test to succeed. Constraints can apply to leafs of the document tree or to nodes depending of the nature of the constraint.
- Parameter
- A parameter is a value that can be used by the constraints to modify their behavior.
- Iteration state
- An iteration state describes how many iterations of each possible level are present
in the run (e.g. idtset = 2, itimimage = not used, image = 5, time = not used).
It gives information on the current state of the run. Documents are implicitly
associated to their iteration state. This information is made available to
the test engine through specialized YAML documents with
IterStart
tag.
Tip
To get the list of constraints and parameters, run:
~abinit/tests/testtools.py explore
and type show *
. You can then type for example show tol_eq
to learn more
about a specific constraint or parameter.
A more complicated example¶
The Etot
document is the simplest possible document. It only contains fields
with real values. Now we will have a look at the ResultsGS
document
that represents the results stored in the corresponding Fortran datatype used in Abinit.
The YAML document is now given by:
--- !ResultsGS
comment : Summary of ground states results.
natom : 5
nsppol : 1
cut : {"ecut": 1.20000000000000000E+01, "pawecutdg": 2.00000000000000000E+01, }
convergence: {
"deltae": 2.37409381043107715E-09, "res2": 1.41518780109792898E-08,
"residm": 2.60254842131463755E-07, "diffor": 0.00000000000000000E+00,
}
etotal : -1.51507711707660150E+02
entropy : 0.00000000000000000E+00
fermie : 3.09658145725792422E-01
stress tensor: !Tensor
- [ 3.56483996349480498E-03, 0.00000000000000000E+00, 0.00000000000000000E+00, ]
- [ 0.00000000000000000E+00, 3.56483996349480151E-03, 0.00000000000000000E+00, ]
- [ 0.00000000000000000E+00, 0.00000000000000000E+00, 3.56483996349478416E-03, ]
cartesian forces: !CartForces
- [ -0.00000000000000000E+00, -0.00000000000000000E+00, -0.00000000000000000E+00, ]
- [ -0.00000000000000000E+00, -0.00000000000000000E+00, -0.00000000000000000E+00, ]
- [ -0.00000000000000000E+00, -0.00000000000000000E+00, -0.00000000000000000E+00, ]
- [ -0.00000000000000000E+00, -0.00000000000000000E+00, -0.00000000000000000E+00, ]
- [ -0.00000000000000000E+00, -0.00000000000000000E+00, -0.00000000000000000E+00, ]
...
This YAML document is more complicated as it contains scalar fields, dictionaries and even 2D arrays.
MG: Are integer values always compared without tolerance?
Still, the parsers will be able to locate the entire document via its tag/label and address all the entries by name.
To specify the tolerance for the relative difference for all the scalar quantities in ResultsGS
,
we just add a new entry to the YAML configuration file similarly to what we did for EnergyTerms
:
EnergyTerms:
tol_abs: 1.0e-7
total_energy_eV:
tol_abs: 1.0e-5
tol_rel: 1.0e-10
ResultsGS:
tol_rel: 1.0e-8
At this point we have to precise that there are implicit top-level settings that are applied on all quantities that are not subject to a more specific rule. For example in the above example ResultsGS is subject to the default absolute tolerance, whereas the default relative tolerance have been overridden.
Unfortunately, such a strict value for tol_rel
will become very problematic
when we have to compare the residues stored in the convergence
dictionary!
In this case, it makes more sense to check that all the residues are below a certain threshold.
This is what the ceil constraint is for:
ResultsGS:
tol_rel: 1.0e-8
convergence:
ceil: 3.0e-7
Now the test will fail if one of the components of the convergence
dictionary is above 3.0e-7.
Note that the ceil
constraint automatically disables the check for tol_rel
and tol_abs
inside convergence
.
In other words, all the scalar entries in ResultsGS
will be compared with our tol_rel
and the default tol_abs
whereas the entries in the convergence
dictionary will be tested against ceil
.
Tip
Within the explore shell show ceil
will list
the constraints that are disabled by the use of ceil
in the exclude field.
Up to now we have been focusing on scalar quantities for which the concept of
relative and absolute difference is unambiguously defined but how do we compare vectors and matrices?
Fields with the !TensorCart
tags are leafs of the tree. The tester routine
won’t try to compare each individual coefficient with tol_rel
. However we
still want to check that it does not change too much. For that purpose we use the
tol_vec
constraint which apply to all arrays derived from BaseArray
(most
arrays with a tag). BaseArray
let us use the capabilities of Numpy arrays with
YAML defined arrays. tol_vec
check the euclidean distance between the reference
and the output arrays. Since we also want to apply this constraint to
cartesian_force
, we will define the constraint at the top level of ResultsGS
.
ResultsGS:
tol_rel: 1.0e-8
tol_vec: 1.0e-5
convergence:
ceil: 3.0e-7
How to use filters to select documents by iteration state¶
Thanks to the syntax presented in the previous sections, one can customize tolerances for different documents and different entries. Note however that these rules will be applied to all the documents found in the output file. This means that we are implicitly assuming that all the different steps of the calculation have similar numerical stability. There are however cases in which the results of particular datasets are less numerically stable than the others. An example will help clarify.
The test paral[86] uses two datasets to perform two different computations. The first dataset computes the DFT density with LDA while the second dataset uses the LDA density to perform a DMFT computation. The entire calculation is supposed to take less than ~3-5 minutes hence the input parameters are severely under converged and the numerical noise propagates quickly through the different steps. As a consequence, one cannot expect the DFMT results to have the same numerical stability as the LDA part. Fortunately, one can use filters to specify different convergence criteria for the two datasets.
A filter is a mechanism that allows one to associate a specific configuration to a set of iteration states.
A filter is defined in a separated section of the configuration file under the node filters
.
Let’s declare two filters with the syntax:
filters:
ks:
dtset: 1
dmft:
dtset: 2
Here we are simply saying that we want to associate the label ks
to all
documents created in the first dataset and the label dmft
to all document
created in the second dataset. The chose of the names ks
and dmft
are
absolutely arbitrary. Pick anything that make sense to your test. This is the
simplest filter declaration possible. See here for more info on
filter declarations. Now we can use our filters. First of all we will associate
the configuration we already wrote to the ks
filter so we can have a different
configuration for the second dataset. The YAML file now reads
filters:
ks:
dtset: 1
dmft:
dtset: 2
ks:
EnergyTerms:
tol_abs: 1.0e-7
total_energy_eV:
tol_abs: 1.0e-5
tol_rel: 1.0e-10
ResultsGS:
tol_rel: 1.0e-8
tol_vec: 1.0e-5
convergence:
ceil: 3.0e-7
By inserting the configuration options under the ks
node, we specify that these rules
apply only to the first dataset. We will then create a new dmft
node and create a
configuration following the same procedure as before.
We end up with something like this:
filters:
ks:
dtset: 1
dmft:
dtset: 2
ks:
EnergyTerms:
tol_abs: 1.0e-7
total_energy_eV:
tol_abs: 1.0e-5
tol_rel: 1.0e-10
ResultsGS:
tol_rel: 1.0e-8
tol_vec: 1.0e-5
convergence:
ceil: 3.0e-7
dmft:
tol_abs: 2.0e-8
tol_rel: 5.0e-9
ResultsGS:
convergence:
ceil: 1.0e-6
diffor:
ignore: true
fermie:
tol_abs: 1.0e-7
tol_rel: 1.0e-8
stress tensor:
ignore: true
EnergyTerms:
total_energy_eV:
tol_abs: 1.0e-5
tol_rel: 1.0e-8
EnergyTermsDC:
tol_abs: 1.0e-7
total_energy_dc_eV:
tol_abs: 1.0e-5
tol_rel: 1.0e-8
Filters API¶
Filters provide a practical way to specify different configuration for different states of iterations without having to rewrite everything from scratch.
Filter declaration¶
A filter can specify all currently known iterators: dtset, timimage, image, and time. For each iterator, a set of integers can be defined with three different methods:
- a single integer value e.g.
dtset: 1
- a YAML list of values e.g.
dtset: [1, 2, 5]
- a mapping with the optional members “from” and “to” specifying the boundaries (both
included) of the integer interval e.g.
dtset: {from: 1, to: 5}
. If “from” is omitted, the default is 1. If “to” is omitted the default is no upper boundary.
Tip
The order is never relevant in parsing YAML (unless you are writing a list of course). As a consequence you can define filter wherever you want in the file.
Filter overlapping¶
Several filters can apply to the same document even when they overlap. Note, however, that overlapping filters must have a trivial order of specificity. In other words, one filter must be a subset of the other one. The example below is OK because f2 is included in f1 i.e. is more specific:
# this is fine
filters:
f1:
dtset:
from: 2
to: 7
image:
from: 4
f2:
dtset: 7
image:
- 4
- 5
- 6
whereas this second example will raise an error because f4 is not included in f3.
# this will raise an error
filters:
f3:
dtset:
from: 2
to: 7
image:
from: 4
f4:
dtset: 7
image:
from: 1
to: 5
When a test is defined, the default tree is overridden by the user-defined tree.
When a filtered tree is used, it overrides the less specific tree. Trees are
sequentially applied to the tree from the most general to the most specific one.
The overriding process is often used, though it is important to know how it
works. By default, only what is explicitly specified in the file is overridden which means
that if a constraint is defined at a deeper level on the default tree than what
is done on the new tree, the original constraints will be kept. For example let
f1
and f2
be two filters such that f2
is included in f1
.
filters:
f1:
dtset: 1
f2:
dtset: 1
image: 5
f1:
ResultsGS:
tol_abs: 1.0e-6
convergence:
ceil: 1.0e-6
diffor:
1.0e-4
f2:
ResultsGS:
tol_rel: 1.0e-7
convergence:
ceil: 1.0e-7
When the tester will reach the fifth image of the first dataset, the config tree used will be the following:
ResultsGS:
tol_abs: 1.0e-6 # this come from application of f1
tol_rel: 1.0e-7 # this has been appended without modifying anything else when appling f2
convergence:
ceil: 1.0e-7 # this one have been overridden
diffor:
1.0e-4 # this one have been kept
If this is not the behavior you need, you can use the “hard reset marker”.
Append !
to the name of the specialization you want to override to completely
replace it. Let the f2
tree be:
f2:
ResultsGS:
convergence!:
ceil: 1.0e-7
and now the resulting tree for the fifth image of the first dataset is:
ResultsGS:
tol_abs: 1.0e-6
convergence: # the whole convergence node have been overriden
ceil: 1.0e-7
Tip
Here again the explore
shell could be of great help to know what is inherited
from the other trees and what is overridden.
How to use equation and callback¶
equation and callback are special constraints because their actual effects are defined directly in the configuration file. They have been introduced to increase the flexibility of the configuration file without having to change the python code.
equation takes a string in input. This string will
be interpreted as a python expression that must return in a number. The
absolute value of this number will be compared to the value of the tol_eq
parameter and if tol_eq
is greater the test will succeed. The expression can
also result in a numpy array. In this case, the returned value if the euclidean norm of the
array that will be compared to tol_eq
value.
A minimal example:
EnergyTerms:
tol_eq: 1.0e-6
equation: 'this["Etotal"] - this["Total energy(eV)"]/27.2114'
equations works exactly the same but has a list of string as value.
Each string is a different expression
that will be tested independently from the others. In both case the tested
object can be referred as this
and the reference object can be referred as ref
.
callback requires a bit of python coding since it will invoke a method of the
structure. Suppose we have a tag !AtomSpeeds
associated to a document and
a class AtomSpeeds
. The AtomSpeeds
class have a method not_going_anywhere
that checks
that the atoms are not going to try to leave the box. We would like to
pass some kind of tolerance d_min
the minimal distance atoms can approach
the border of the box. The signature of the method have to be
not_going_anywhere(self, tested, d_min=DEFAULT_VALUE)
and should return
True
, False
or an instance of FailDetail
(see Add a new constraint
for explanations about those). Note that self
will be the reference
instance. We can then use it by with the following configuration:
AtomSpeeds:
callback:
method: not_going_anywhere
d_min: 1.0e-2
Command line interface¶
The ~abinit/tests/testtools.py
script provides a command line interface to facilitate
the creation of new tests and the exploration of the YAML configuration file.
The syntax is:
./testtools.py COMMAND [options]
Run the script without arguments to get the list of possible commands and use:
./testtools.py COMMAND --help
to display the options supported by COMMAND
.
The list of available commands is:
- fldiff
-
Interface to the fldiff.py module. This command can be used to compare output and reference files without executing ABINIT. It is also possible to specify the YAML configuration file with the
--yaml-conf
option so that one can employ the same parameters as those used by runtests.py - explore
-
This command allows the user to explore and validate a YAML configuration file. It provides a shell like interface in which the user can explore the tree defined by the configuration file and print the constraints. It also provides documentation about constraints and parameters via the show command.