kim-tools 0.3.13__py3-none-any.whl → 0.4.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kim_tools/__init__.py +1 -1
- kim_tools/aflow_util/core.py +4 -12
- kim_tools/kimunits.py +26 -10
- kim_tools/symmetry_util/core.py +64 -114
- kim_tools/test_driver/core.py +202 -27
- {kim_tools-0.3.13.dist-info → kim_tools-0.4.3.dist-info}/METADATA +2 -1
- {kim_tools-0.3.13.dist-info → kim_tools-0.4.3.dist-info}/RECORD +10 -10
- {kim_tools-0.3.13.dist-info → kim_tools-0.4.3.dist-info}/WHEEL +0 -0
- {kim_tools-0.3.13.dist-info → kim_tools-0.4.3.dist-info}/licenses/LICENSE.CDDL +0 -0
- {kim_tools-0.3.13.dist-info → kim_tools-0.4.3.dist-info}/top_level.txt +0 -0
kim_tools/__init__.py
CHANGED
kim_tools/aflow_util/core.py
CHANGED
|
@@ -17,7 +17,6 @@ import numpy as np
|
|
|
17
17
|
import numpy.typing as npt
|
|
18
18
|
from ase import Atoms
|
|
19
19
|
from ase.cell import Cell
|
|
20
|
-
from ase.neighborlist import natural_cutoffs, neighbor_list
|
|
21
20
|
from semver import Version
|
|
22
21
|
from sympy import Symbol, linear_eq_to_matrix, matrix2numpy, parse_expr
|
|
23
22
|
|
|
@@ -30,6 +29,7 @@ from ..symmetry_util import (
|
|
|
30
29
|
cartesian_rotation_is_in_point_group,
|
|
31
30
|
get_possible_primitive_shifts,
|
|
32
31
|
get_primitive_wyckoff_multiplicity,
|
|
32
|
+
get_smallest_nn_dist,
|
|
33
33
|
get_wyck_pos_xform_under_normalizer,
|
|
34
34
|
space_group_numbers_are_enantiomorphic,
|
|
35
35
|
)
|
|
@@ -1605,20 +1605,12 @@ class AFLOW:
|
|
|
1605
1605
|
"""
|
|
1606
1606
|
# If max_resid not provided, determine it from neighborlist
|
|
1607
1607
|
if max_resid is None:
|
|
1608
|
-
nl_len = 0
|
|
1609
|
-
cov_mult = 1
|
|
1610
|
-
while nl_len == 0:
|
|
1611
|
-
logger.info(
|
|
1612
|
-
"Attempting to find NN distance by searching "
|
|
1613
|
-
f"within covalent radii times {cov_mult}"
|
|
1614
|
-
)
|
|
1615
|
-
nl = neighbor_list("d", atoms, natural_cutoffs(atoms, mult=cov_mult))
|
|
1616
|
-
nl_len = nl.size
|
|
1617
|
-
cov_mult += 1
|
|
1618
1608
|
# set the maximum error to 1% of NN distance to follow AFLOW convention
|
|
1619
1609
|
# rescale by cube root of cell volume to get rough conversion from
|
|
1620
1610
|
# cartesian to fractional
|
|
1621
|
-
max_resid =
|
|
1611
|
+
max_resid = (
|
|
1612
|
+
get_smallest_nn_dist(atoms) * 0.01 * atoms.get_volume() ** (-1 / 3)
|
|
1613
|
+
)
|
|
1622
1614
|
logger.info(
|
|
1623
1615
|
"Automatically set max fractional residual for solving position "
|
|
1624
1616
|
f"equations to {max_resid}"
|
kim_tools/kimunits.py
CHANGED
|
@@ -12,6 +12,8 @@ import re
|
|
|
12
12
|
import subprocess
|
|
13
13
|
import warnings
|
|
14
14
|
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
15
17
|
warnings.simplefilter("ignore")
|
|
16
18
|
|
|
17
19
|
|
|
@@ -24,17 +26,31 @@ _units_output_expression = re.compile(
|
|
|
24
26
|
)
|
|
25
27
|
|
|
26
28
|
|
|
27
|
-
def check_units_util():
|
|
29
|
+
def check_units_util() -> str:
|
|
28
30
|
"""
|
|
29
|
-
|
|
31
|
+
Figure out if units (first choice) or gunits (second choice) works
|
|
32
|
+
with the options that we use
|
|
30
33
|
"""
|
|
34
|
+
args = ["-o", r"%1.15e", "-qt1", "0.0 eV/angstrom^3 bar"]
|
|
31
35
|
try:
|
|
32
|
-
subprocess.check_output(["units", "
|
|
36
|
+
output = subprocess.check_output(["units"] + args, encoding="utf-8")
|
|
37
|
+
assert np.isclose(float(output), 0)
|
|
38
|
+
units_command = "units"
|
|
33
39
|
except Exception:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
try:
|
|
41
|
+
output = subprocess.check_output(["gunits"] + args, encoding="utf-8")
|
|
42
|
+
assert np.isclose(float(output), 0)
|
|
43
|
+
units_command = "gunits"
|
|
44
|
+
except Exception:
|
|
45
|
+
raise UnitConversion(
|
|
46
|
+
"Neither "
|
|
47
|
+
r"units -o %1.15e -qt1 '0.0 eV/angstrom^3 bar'"
|
|
48
|
+
" nor "
|
|
49
|
+
r"gunits -o %1.15e -qt1 '0.0 eV/angstrom^3 bar'"
|
|
50
|
+
" successfully ran and returned 0.e0. Please install a "
|
|
51
|
+
"compatible version of units."
|
|
52
|
+
)
|
|
53
|
+
return units_command
|
|
38
54
|
|
|
39
55
|
|
|
40
56
|
def linear_fit(x, y):
|
|
@@ -68,7 +84,7 @@ def islinear(unit, to_unit=None):
|
|
|
68
84
|
|
|
69
85
|
def convert_units(from_value, from_unit, wanted_unit=None, suppress_unit=False):
|
|
70
86
|
"""Works with 'units' utility"""
|
|
71
|
-
check_units_util()
|
|
87
|
+
units_util = check_units_util()
|
|
72
88
|
from_sign = from_value < 0
|
|
73
89
|
from_value = str(abs(from_value))
|
|
74
90
|
from_unit = str(from_unit)
|
|
@@ -77,7 +93,7 @@ def convert_units(from_value, from_unit, wanted_unit=None, suppress_unit=False):
|
|
|
77
93
|
|
|
78
94
|
if from_unit in TEMPERATURE_FUNCTION_UNITS:
|
|
79
95
|
args = [
|
|
80
|
-
|
|
96
|
+
units_util,
|
|
81
97
|
"-o",
|
|
82
98
|
"%1.15e",
|
|
83
99
|
"-qt1",
|
|
@@ -85,7 +101,7 @@ def convert_units(from_value, from_unit, wanted_unit=None, suppress_unit=False):
|
|
|
85
101
|
]
|
|
86
102
|
|
|
87
103
|
else:
|
|
88
|
-
args = [
|
|
104
|
+
args = [units_util, "-o", "%1.15e", "-qt1", " ".join((from_value, from_unit))]
|
|
89
105
|
|
|
90
106
|
if wanted_unit:
|
|
91
107
|
args.append(wanted_unit)
|
kim_tools/symmetry_util/core.py
CHANGED
|
@@ -9,7 +9,6 @@ from itertools import product
|
|
|
9
9
|
from math import ceil
|
|
10
10
|
from typing import Dict, List, Optional, Tuple, Union
|
|
11
11
|
|
|
12
|
-
import matplotlib.pyplot as plt
|
|
13
12
|
import numpy as np
|
|
14
13
|
import numpy.typing as npt
|
|
15
14
|
import sympy as sp
|
|
@@ -17,11 +16,9 @@ from ase import Atoms
|
|
|
17
16
|
from ase.cell import Cell
|
|
18
17
|
from ase.constraints import FixSymmetry
|
|
19
18
|
from ase.geometry import get_distances, get_duplicate_atoms
|
|
20
|
-
from
|
|
19
|
+
from ase.neighborlist import natural_cutoffs, neighbor_list
|
|
21
20
|
from pymatgen.core.operations import SymmOp
|
|
22
21
|
from pymatgen.core.tensors import Tensor
|
|
23
|
-
from scipy.stats import kstest
|
|
24
|
-
from sklearn.decomposition import PCA
|
|
25
22
|
from sympy import Matrix, cos, matrix2numpy, sin, sqrt, symbols
|
|
26
23
|
from sympy.tensor.array.expressions import ArrayContraction, ArrayTensorProduct
|
|
27
24
|
|
|
@@ -50,6 +47,7 @@ __all__ = [
|
|
|
50
47
|
"change_of_basis_atoms",
|
|
51
48
|
"get_possible_primitive_shifts",
|
|
52
49
|
"get_primitive_genpos_ops",
|
|
50
|
+
"get_smallest_nn_dist",
|
|
53
51
|
]
|
|
54
52
|
|
|
55
53
|
C_CENTERED_ORTHORHOMBIC_GROUPS = (20, 21, 35, 36, 37, 63, 64, 65, 66, 67, 68)
|
|
@@ -516,7 +514,9 @@ def get_change_of_basis_matrix_to_conventional_cell_from_formal_bravais_lattice(
|
|
|
516
514
|
return np.round(change_of_basis_matrix)
|
|
517
515
|
|
|
518
516
|
|
|
519
|
-
def change_of_basis_atoms(
|
|
517
|
+
def change_of_basis_atoms(
|
|
518
|
+
atoms: Atoms, change_of_basis: npt.ArrayLike, cutoff: Optional[float] = None
|
|
519
|
+
) -> Atoms:
|
|
520
520
|
"""
|
|
521
521
|
Perform an arbitrary basis change on an ``Atoms`` object, duplicating or cropping
|
|
522
522
|
atoms as needed. A basic check is made that the determinant of ``change_of_basis``
|
|
@@ -524,7 +524,7 @@ def change_of_basis_atoms(atoms: Atoms, change_of_basis: npt.ArrayLike) -> Atoms
|
|
|
524
524
|
that ``change_of_basis`` is appropriate for the particuar crystal described by
|
|
525
525
|
``atoms``, which is up to the user.
|
|
526
526
|
|
|
527
|
-
TODO: Incorporate
|
|
527
|
+
TODO: Incorporate period extension test into this function
|
|
528
528
|
|
|
529
529
|
Args:
|
|
530
530
|
atoms:
|
|
@@ -543,6 +543,9 @@ def change_of_basis_atoms(atoms: Atoms, change_of_basis: npt.ArrayLike) -> Atoms
|
|
|
543
543
|
|
|
544
544
|
Relationship between fractional coordinates in each basis:
|
|
545
545
|
**x** = **P** **x**'
|
|
546
|
+
cutoff:
|
|
547
|
+
The cutoff to use for deleting duplicate atoms. If not specified,
|
|
548
|
+
the AFLOW tolerance of 0.01*(smallest NN distance) is used.
|
|
546
549
|
|
|
547
550
|
Returns:
|
|
548
551
|
The transformed ``Atoms`` object, containing the original number of
|
|
@@ -577,7 +580,9 @@ def change_of_basis_atoms(atoms: Atoms, change_of_basis: npt.ArrayLike) -> Atoms
|
|
|
577
580
|
new_atoms = atoms.repeat(repeat)
|
|
578
581
|
new_atoms.set_cell(new_cell)
|
|
579
582
|
new_atoms.wrap()
|
|
580
|
-
|
|
583
|
+
if cutoff is None:
|
|
584
|
+
cutoff = get_smallest_nn_dist(atoms) * 0.01
|
|
585
|
+
get_duplicate_atoms(new_atoms, cutoff=cutoff, delete=True)
|
|
581
586
|
|
|
582
587
|
volume_change = np.linalg.det(change_of_basis)
|
|
583
588
|
if not np.isclose(len(atoms) * volume_change, len(new_atoms)):
|
|
@@ -634,14 +639,16 @@ def transform_atoms(atoms: Atoms, op: Dict) -> Atoms:
|
|
|
634
639
|
return atoms_transformed
|
|
635
640
|
|
|
636
641
|
|
|
637
|
-
def reduce_and_avg(
|
|
638
|
-
atoms: Atoms, repeat: Tuple[int, int, int]
|
|
639
|
-
) -> Tuple[Atoms, npt.ArrayLike]:
|
|
642
|
+
def reduce_and_avg(atoms: Atoms, repeat: Tuple[int, int, int]) -> Atoms:
|
|
640
643
|
"""
|
|
641
644
|
TODO: Upgrade :func:`change_of_basis_atoms` to provide the distances
|
|
642
645
|
array, obviating this function
|
|
643
646
|
|
|
644
|
-
Function to reduce all atoms to the original unit cell position
|
|
647
|
+
Function to reduce all atoms to the original unit cell position,
|
|
648
|
+
assuming the supercell is built from contiguous repeats of the unit cell
|
|
649
|
+
(i.e. atoms 0 to N-1 in the supercell are the original unit cell, atoms N to
|
|
650
|
+
2*[N-1] are the original unit cell shifted by an integer multiple of
|
|
651
|
+
the lattice vectors, and so on)
|
|
645
652
|
|
|
646
653
|
Args:
|
|
647
654
|
atoms:
|
|
@@ -651,10 +658,13 @@ def reduce_and_avg(
|
|
|
651
658
|
provided supercell
|
|
652
659
|
|
|
653
660
|
Returns:
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
661
|
+
The reduced unit cell
|
|
662
|
+
|
|
663
|
+
Raises:
|
|
664
|
+
PeriodExtensionException:
|
|
665
|
+
If two atoms that should be identical by translational symmetry
|
|
666
|
+
are further than 0.01*(smallest NN distance) apart when
|
|
667
|
+
reduced to the unit cell
|
|
658
668
|
"""
|
|
659
669
|
new_atoms = atoms.copy()
|
|
660
670
|
|
|
@@ -685,126 +695,49 @@ def reduce_and_avg(
|
|
|
685
695
|
# Start from end of the atoms
|
|
686
696
|
# because we will remove all atoms except the reference ones.
|
|
687
697
|
for i in reversed(range(number_atoms)):
|
|
698
|
+
reference_atom_index = i % original_number_atoms
|
|
688
699
|
if i >= original_number_atoms:
|
|
689
700
|
# Get the distance to the reference atom in the original unit cell with the
|
|
690
701
|
# minimum image convention.
|
|
691
702
|
distance = new_atoms.get_distance(
|
|
692
|
-
|
|
703
|
+
reference_atom_index, i, mic=True, vector=True
|
|
693
704
|
)
|
|
694
705
|
# Get the position that has the closest distance to
|
|
695
706
|
# the reference atom in the original unit cell.
|
|
696
|
-
position_i = positions[
|
|
707
|
+
position_i = positions[reference_atom_index] + distance
|
|
697
708
|
# Remove atom from atoms object.
|
|
698
709
|
new_atoms.pop()
|
|
699
710
|
else:
|
|
700
711
|
# Atom was part of the original unit cell.
|
|
701
712
|
position_i = positions[i]
|
|
702
713
|
# Average
|
|
703
|
-
avg_positions_in_prim_cell[
|
|
714
|
+
avg_positions_in_prim_cell[reference_atom_index] += position_i / M
|
|
704
715
|
positions_in_prim_cell[i] = position_i
|
|
705
716
|
|
|
706
717
|
new_atoms.set_positions(avg_positions_in_prim_cell)
|
|
707
718
|
|
|
708
|
-
#
|
|
709
|
-
|
|
710
|
-
for
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
719
|
+
# Check that all atoms are within tolerance of their translational images
|
|
720
|
+
cutoff = get_smallest_nn_dist(new_atoms) * 0.01
|
|
721
|
+
logger.info(f"Cutoff for period extension test is {cutoff}")
|
|
722
|
+
for i in range(original_number_atoms):
|
|
723
|
+
positions_of_all_images_of_atom_i = [
|
|
724
|
+
positions_in_prim_cell[j * original_number_atoms + i] for j in range(M)
|
|
725
|
+
]
|
|
726
|
+
_, r = get_distances(
|
|
727
|
+
positions_of_all_images_of_atom_i,
|
|
714
728
|
cell=new_atoms.get_cell(),
|
|
715
729
|
pbc=True,
|
|
716
730
|
)
|
|
717
|
-
#
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
reduced_distances: npt.ArrayLike,
|
|
726
|
-
significance_level: float = 0.05,
|
|
727
|
-
plot_filename: Optional[str] = None,
|
|
728
|
-
number_bins: Optional[int] = None,
|
|
729
|
-
) -> None:
|
|
730
|
-
"""
|
|
731
|
-
TODO: Incorporate this into :func:`change_of_basis_atoms`
|
|
732
|
-
|
|
733
|
-
Function to test whether the reduced atom positions are normally distributed
|
|
734
|
-
around their average.
|
|
735
|
-
|
|
736
|
-
Args:
|
|
737
|
-
reduced_distances:
|
|
738
|
-
Distance array provided by :func:`reduce_and_avg`
|
|
739
|
-
significance_level:
|
|
740
|
-
Significance level for Kolmogorov-Smirnov
|
|
741
|
-
plot_filename:
|
|
742
|
-
number_bins:
|
|
743
|
-
Number of bins for plot
|
|
744
|
-
|
|
745
|
-
Raises:
|
|
746
|
-
PeriodExtensionException:
|
|
747
|
-
If a non-normal distribution is detected
|
|
748
|
-
"""
|
|
749
|
-
assert len(reduced_distances.shape) == 3
|
|
750
|
-
assert reduced_distances.shape[2] == 3
|
|
751
|
-
|
|
752
|
-
if plot_filename is not None:
|
|
753
|
-
if number_bins is None:
|
|
754
|
-
raise ValueError(
|
|
755
|
-
"number_bins must be specified if plot_filename is specified"
|
|
731
|
+
# Checking full MxM matrix, could probably speed up by
|
|
732
|
+
# checking upper triangle only. Could also save memory
|
|
733
|
+
# by looping over individual distances instead of
|
|
734
|
+
# checking the max of a giant matrix
|
|
735
|
+
assert r.shape == (M, M)
|
|
736
|
+
if r.max() > cutoff:
|
|
737
|
+
raise PeriodExtensionException(
|
|
738
|
+
f"At least one image of atom {i} is outside of tolerance"
|
|
756
739
|
)
|
|
757
|
-
|
|
758
|
-
raise ValueError(f"{plot_filename} is not a PDF file")
|
|
759
|
-
with PdfPages(plot_filename) as pdf:
|
|
760
|
-
for i in range(reduced_distances.shape[0]):
|
|
761
|
-
fig, axs = plt.subplots(1, 3, figsize=(10.0, 4.0))
|
|
762
|
-
for j in range(reduced_distances.shape[2]):
|
|
763
|
-
axs[j].hist(reduced_distances[i, :, j], bins=number_bins)
|
|
764
|
-
axs[j].set_xlabel(f"$x_{j}$")
|
|
765
|
-
axs[0].set_ylabel("Counts")
|
|
766
|
-
fig.suptitle(f"Atom {i}")
|
|
767
|
-
pdf.savefig()
|
|
768
|
-
else:
|
|
769
|
-
if number_bins is not None:
|
|
770
|
-
raise ValueError(
|
|
771
|
-
"number_bins must not be specified if plot_filename is not specified"
|
|
772
|
-
)
|
|
773
|
-
|
|
774
|
-
p_values = np.empty((reduced_distances.shape[0], reduced_distances.shape[2]))
|
|
775
|
-
for i in range(reduced_distances.shape[0]):
|
|
776
|
-
atom_distances = reduced_distances[i]
|
|
777
|
-
|
|
778
|
-
# Perform PCA on the xyz distribution.
|
|
779
|
-
pca = PCA(n_components=atom_distances.shape[1])
|
|
780
|
-
pca_components = pca.fit_transform(atom_distances)
|
|
781
|
-
assert (
|
|
782
|
-
pca_components.shape == atom_distances.shape == reduced_distances.shape[1:]
|
|
783
|
-
)
|
|
784
|
-
|
|
785
|
-
# Test each component with a KS test.
|
|
786
|
-
for j in range(pca_components.shape[1]):
|
|
787
|
-
component = pca_components[:, j]
|
|
788
|
-
component_mean = np.mean(component)
|
|
789
|
-
assert abs(component_mean) < 1.0e-7
|
|
790
|
-
component_std = np.std(component)
|
|
791
|
-
# Normalize component
|
|
792
|
-
normalized_component = (component - component_mean) / component_std
|
|
793
|
-
assert abs(np.mean(normalized_component)) < 1.0e-7
|
|
794
|
-
assert abs(np.std(normalized_component) - 1.0) < 1.0e-7
|
|
795
|
-
res = kstest(normalized_component, "norm")
|
|
796
|
-
p_values[i, j] = res.pvalue
|
|
797
|
-
|
|
798
|
-
if np.any(p_values <= significance_level):
|
|
799
|
-
raise PeriodExtensionException(
|
|
800
|
-
"Detected non-normal distribution of reduced atom positions around their "
|
|
801
|
-
f"average (smallest p value {np.min(p_values)})."
|
|
802
|
-
)
|
|
803
|
-
else:
|
|
804
|
-
print(
|
|
805
|
-
"Detected normal distribution or reduced atom positions around their "
|
|
806
|
-
f"average (smallest p value {np.min(p_values)})."
|
|
807
|
-
)
|
|
740
|
+
return new_atoms
|
|
808
741
|
|
|
809
742
|
|
|
810
743
|
def voigt_to_full_symb(voigt_input: sp.Array) -> sp.MutableDenseNDimArray:
|
|
@@ -1073,6 +1006,23 @@ def fit_voigt_tensor_to_cell_and_space_group(
|
|
|
1073
1006
|
return t_symmetrized.voigt
|
|
1074
1007
|
|
|
1075
1008
|
|
|
1009
|
+
def get_smallest_nn_dist(atoms: Atoms) -> float:
|
|
1010
|
+
"""
|
|
1011
|
+
Get the smallest NN distance in an Atoms object
|
|
1012
|
+
"""
|
|
1013
|
+
nl_len = 0
|
|
1014
|
+
cov_mult = 1
|
|
1015
|
+
while nl_len == 0:
|
|
1016
|
+
logger.info(
|
|
1017
|
+
"Attempting to find NN distance by searching "
|
|
1018
|
+
f"within covalent radii times {cov_mult}"
|
|
1019
|
+
)
|
|
1020
|
+
nl = neighbor_list("d", atoms, natural_cutoffs(atoms, mult=cov_mult))
|
|
1021
|
+
nl_len = nl.size
|
|
1022
|
+
cov_mult += 1
|
|
1023
|
+
return nl.min()
|
|
1024
|
+
|
|
1025
|
+
|
|
1076
1026
|
class FixProvidedSymmetry(FixSymmetry):
|
|
1077
1027
|
"""
|
|
1078
1028
|
A modification of :obj:`~ase.constraints.FixSymmetry` that takes
|
kim_tools/test_driver/core.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
###############################################################################
|
|
2
2
|
#
|
|
3
3
|
# CDDL HEADER START
|
|
4
4
|
#
|
|
@@ -30,14 +30,17 @@
|
|
|
30
30
|
Helper classes for KIM Test Drivers
|
|
31
31
|
|
|
32
32
|
"""
|
|
33
|
-
import glob
|
|
34
33
|
import json
|
|
35
34
|
import logging
|
|
36
35
|
import os
|
|
37
36
|
import shutil
|
|
37
|
+
import tarfile
|
|
38
38
|
from abc import ABC, abstractmethod
|
|
39
39
|
from copy import deepcopy
|
|
40
|
+
from fnmatch import fnmatch
|
|
41
|
+
from glob import glob
|
|
40
42
|
from pathlib import Path
|
|
43
|
+
from secrets import token_bytes
|
|
41
44
|
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
|
42
45
|
from typing import IO, Any, Dict, List, Optional, Union
|
|
43
46
|
|
|
@@ -109,6 +112,23 @@ PROP_SEARCH_PATHS_INFO = (
|
|
|
109
112
|
"- $PWD/local-props/**/\n"
|
|
110
113
|
"- $PWD/local_props/**/"
|
|
111
114
|
)
|
|
115
|
+
TOKEN_NAME = "kim-tools.token"
|
|
116
|
+
TOKENPATH = os.path.join("output", TOKEN_NAME)
|
|
117
|
+
PIPELINE_EXCEPTIONS = ["output/pipeline.*"]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _glob_with_exceptions(
|
|
121
|
+
pattern: str, exceptions: List[str], recursive: bool = False
|
|
122
|
+
) -> List[os.PathLike]:
|
|
123
|
+
"""
|
|
124
|
+
Return a list of paths that match the glob "pattern" but not
|
|
125
|
+
the glob "exceptions"
|
|
126
|
+
"""
|
|
127
|
+
match = glob(pattern, recursive=recursive)
|
|
128
|
+
# If we are willing to make Python 3.14 minimum, can use filterfalse
|
|
129
|
+
for exception in exceptions:
|
|
130
|
+
match = [n for n in match if not fnmatch(n, exception)]
|
|
131
|
+
return match
|
|
112
132
|
|
|
113
133
|
|
|
114
134
|
def get_supported_lammps_atom_style(model: str) -> str:
|
|
@@ -395,8 +415,15 @@ def _add_property_instance(
|
|
|
395
415
|
property_name = path_str
|
|
396
416
|
found_custom_property = True
|
|
397
417
|
break
|
|
398
|
-
except Exception:
|
|
399
|
-
|
|
418
|
+
except Exception as e:
|
|
419
|
+
msg = (
|
|
420
|
+
"MESSAGE: Trying to load a property from the .edn file at\n"
|
|
421
|
+
f"{path}\n"
|
|
422
|
+
"failed with the following exception:\n"
|
|
423
|
+
f"{repr(e)}"
|
|
424
|
+
)
|
|
425
|
+
logger.info(msg)
|
|
426
|
+
print(msg)
|
|
400
427
|
|
|
401
428
|
if not found_custom_property:
|
|
402
429
|
raise KIMTestDriverError(
|
|
@@ -550,6 +577,22 @@ class KIMTestDriver(ABC):
|
|
|
550
577
|
__output_property_instances (str):
|
|
551
578
|
Property instances, possibly accumulated over multiple invocations of
|
|
552
579
|
the Test Driver
|
|
580
|
+
__files_to_keep_in_output (List[PathLike]):
|
|
581
|
+
List of files that were written by this class explicitly, that we won't
|
|
582
|
+
touch when cleaning and backing up the output directory. Specified
|
|
583
|
+
relative to 'output' directory.
|
|
584
|
+
__files_to_ignore_in_output (List[PathLike]):
|
|
585
|
+
List of globs of files to ignore when handling the output directory.
|
|
586
|
+
By default, this is set to the constant PIPELINE_EXCEPTIONS,
|
|
587
|
+
which contains files that need to be left untouched for the
|
|
588
|
+
OpenKIM pipeline. Top-level dotfiles are always ignored.
|
|
589
|
+
__token (Optional[bytes]):
|
|
590
|
+
Token that is written to TOKENPATH upon first evaluation. This
|
|
591
|
+
is used to check that multiple Test Drivers are not being called
|
|
592
|
+
concurrently, causing potential conflicts in the output directory
|
|
593
|
+
__times_called (Optional[int]):
|
|
594
|
+
Count of number of times the instance has been __call__'ed,
|
|
595
|
+
for numbering aux file archives
|
|
553
596
|
"""
|
|
554
597
|
|
|
555
598
|
class NonKIMModelError(Exception):
|
|
@@ -560,7 +603,10 @@ class KIMTestDriver(ABC):
|
|
|
560
603
|
"""
|
|
561
604
|
|
|
562
605
|
def __init__(
|
|
563
|
-
self,
|
|
606
|
+
self,
|
|
607
|
+
model: Union[str, Calculator],
|
|
608
|
+
suppr_sm_lmp_log: bool = False,
|
|
609
|
+
files_to_ignore_in_output: List[str] = PIPELINE_EXCEPTIONS,
|
|
564
610
|
) -> None:
|
|
565
611
|
"""
|
|
566
612
|
Args:
|
|
@@ -568,6 +614,9 @@ class KIMTestDriver(ABC):
|
|
|
568
614
|
ASE calculator or KIM model name to use
|
|
569
615
|
suppr_sm_lmp_log:
|
|
570
616
|
Suppress writing a lammps.log
|
|
617
|
+
files_to_ignore_in_output (List[PathLike]):
|
|
618
|
+
List of globs of files to ignore when handling the output directory.
|
|
619
|
+
Top-level dotfiles are always ignored.
|
|
571
620
|
"""
|
|
572
621
|
if isinstance(model, Calculator):
|
|
573
622
|
self.__calc = model
|
|
@@ -582,6 +631,10 @@ class KIMTestDriver(ABC):
|
|
|
582
631
|
self.__calc.parameters.log_file = None
|
|
583
632
|
|
|
584
633
|
self.__output_property_instances = "[]"
|
|
634
|
+
self.__files_to_keep_in_output = []
|
|
635
|
+
self.__files_to_ignore_in_output = files_to_ignore_in_output
|
|
636
|
+
self.__token = None
|
|
637
|
+
self.__times_called = None
|
|
585
638
|
|
|
586
639
|
def _setup(self, material, **kwargs) -> None:
|
|
587
640
|
"""
|
|
@@ -589,6 +642,123 @@ class KIMTestDriver(ABC):
|
|
|
589
642
|
"""
|
|
590
643
|
pass
|
|
591
644
|
|
|
645
|
+
def _init_output_dir(self) -> None:
|
|
646
|
+
"""
|
|
647
|
+
Initialize the output directory
|
|
648
|
+
"""
|
|
649
|
+
if self.__token is None:
|
|
650
|
+
# First time we've called this instance of the class
|
|
651
|
+
assert len(self.property_instances) == 0
|
|
652
|
+
assert self.__times_called is None
|
|
653
|
+
|
|
654
|
+
self.__times_called = 0
|
|
655
|
+
|
|
656
|
+
os.makedirs("output", exist_ok=True)
|
|
657
|
+
|
|
658
|
+
# Move all top-level non-hidden files and directories
|
|
659
|
+
# to backup
|
|
660
|
+
output_glob = _glob_with_exceptions(
|
|
661
|
+
"output/*", self.__files_to_ignore_in_output
|
|
662
|
+
)
|
|
663
|
+
if len(output_glob) > 0:
|
|
664
|
+
i = 0
|
|
665
|
+
while os.path.exists(f"output.{i}"):
|
|
666
|
+
i += 1
|
|
667
|
+
output_bak_name = f"output.{i}"
|
|
668
|
+
msg = (
|
|
669
|
+
"'output' directory has files besides dotfiles and allowed "
|
|
670
|
+
"exceptions, backing up all "
|
|
671
|
+
f"non-hidden files and directories to {output_bak_name}"
|
|
672
|
+
)
|
|
673
|
+
print(msg)
|
|
674
|
+
logger.info(msg)
|
|
675
|
+
os.mkdir(output_bak_name)
|
|
676
|
+
for file_in_output in output_glob:
|
|
677
|
+
shutil.move(file_in_output, output_bak_name)
|
|
678
|
+
|
|
679
|
+
# Create token
|
|
680
|
+
self.__token = token_bytes(16)
|
|
681
|
+
with open(TOKENPATH, "wb") as f:
|
|
682
|
+
f.write(self.__token)
|
|
683
|
+
self.__files_to_keep_in_output.append(TOKEN_NAME)
|
|
684
|
+
else:
|
|
685
|
+
# Token is stored, check that it matches the token file
|
|
686
|
+
if not os.path.isfile(TOKENPATH):
|
|
687
|
+
raise KIMTestDriverError(
|
|
688
|
+
f"Token file at {TOKENPATH} was not found,"
|
|
689
|
+
"can't confirm non-interference of Test Drivers. Did something "
|
|
690
|
+
"edit the 'output' directory between calls to this Test Driver?"
|
|
691
|
+
)
|
|
692
|
+
else:
|
|
693
|
+
with open(TOKENPATH, "rb") as f:
|
|
694
|
+
if self.__token != f.read():
|
|
695
|
+
raise KIMTestDriverError(
|
|
696
|
+
f"Token file at {TOKENPATH} does not match this object's "
|
|
697
|
+
"token. This likely means that a different KIMTestDriver "
|
|
698
|
+
"instance was called between calls to this one. In order to"
|
|
699
|
+
" prevent conflicts in the output directory, this is not "
|
|
700
|
+
"allowed."
|
|
701
|
+
)
|
|
702
|
+
self.__times_called += 1
|
|
703
|
+
|
|
704
|
+
# We should have a record of all non-hidden files in output. If any
|
|
705
|
+
# untracked files are present, raise an error
|
|
706
|
+
output_glob = _glob_with_exceptions(
|
|
707
|
+
"output/**", self.__files_to_ignore_in_output, True
|
|
708
|
+
)
|
|
709
|
+
for filepath in output_glob:
|
|
710
|
+
if os.path.isfile(filepath): # not tracking directories
|
|
711
|
+
if (
|
|
712
|
+
os.path.relpath(filepath, "output")
|
|
713
|
+
not in self.__files_to_keep_in_output
|
|
714
|
+
):
|
|
715
|
+
raise KIMTestDriverError(
|
|
716
|
+
f"Unknown file {filepath} in 'output' directory appeared "
|
|
717
|
+
"between calls to this Test Driver. This is not allowed "
|
|
718
|
+
"because stray files can cause issues."
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
def _archive_aux_files(self) -> None:
|
|
722
|
+
"""
|
|
723
|
+
Archive aux files after a run
|
|
724
|
+
"""
|
|
725
|
+
# Archive untracked files as aux files
|
|
726
|
+
tar_prefix = f"aux_files.{self.__times_called}"
|
|
727
|
+
archive_name = f"output/{tar_prefix}.txz"
|
|
728
|
+
assert not os.path.isfile(tar_prefix)
|
|
729
|
+
output_glob = _glob_with_exceptions(
|
|
730
|
+
"output/**", self.__files_to_ignore_in_output, True
|
|
731
|
+
)
|
|
732
|
+
archived_files = [] # For deleting them later, and checking that any exist
|
|
733
|
+
for filepath in output_glob:
|
|
734
|
+
if os.path.isfile(filepath): # not tracking directories
|
|
735
|
+
output_relpath = os.path.relpath(filepath, "output")
|
|
736
|
+
if output_relpath not in self.__files_to_keep_in_output:
|
|
737
|
+
archived_files.append(filepath)
|
|
738
|
+
|
|
739
|
+
if len(archived_files) > 0:
|
|
740
|
+
msg = f"Auxiliary files found after call, archiving them to {archive_name}"
|
|
741
|
+
print(msg)
|
|
742
|
+
logger.info(msg)
|
|
743
|
+
|
|
744
|
+
with tarfile.open(archive_name, "w:xz") as tar:
|
|
745
|
+
for filepath in archived_files:
|
|
746
|
+
output_relpath = os.path.relpath(filepath, "output")
|
|
747
|
+
tar.add(
|
|
748
|
+
os.path.join(filepath),
|
|
749
|
+
os.path.join(tar_prefix, output_relpath),
|
|
750
|
+
)
|
|
751
|
+
self.__files_to_keep_in_output.append(f"{tar_prefix}.txz")
|
|
752
|
+
for filepath in archived_files:
|
|
753
|
+
os.remove(filepath)
|
|
754
|
+
try:
|
|
755
|
+
os.removedirs(os.path.dirname(filepath))
|
|
756
|
+
except OSError:
|
|
757
|
+
pass # might not be empty yet
|
|
758
|
+
|
|
759
|
+
# should not have removed output dir in any situation
|
|
760
|
+
assert os.path.isdir("output")
|
|
761
|
+
|
|
592
762
|
@abstractmethod
|
|
593
763
|
def _calculate(self, **kwargs) -> None:
|
|
594
764
|
"""
|
|
@@ -601,10 +771,12 @@ class KIMTestDriver(ABC):
|
|
|
601
771
|
|
|
602
772
|
Main operation of a Test Driver:
|
|
603
773
|
|
|
774
|
+
* Call :func:`~KIMTestDriver._init_output_dir`
|
|
604
775
|
* Run :func:`~KIMTestDriver._setup` (the base class provides a barebones
|
|
605
776
|
version, derived classes may override)
|
|
606
777
|
* Call :func:`~KIMTestDriver._calculate` (implemented by each individual
|
|
607
778
|
Test Driver)
|
|
779
|
+
* Call :func:`~KIMTestDriver._archive_aux_files`
|
|
608
780
|
|
|
609
781
|
Args:
|
|
610
782
|
material:
|
|
@@ -614,20 +786,26 @@ class KIMTestDriver(ABC):
|
|
|
614
786
|
Returns:
|
|
615
787
|
The property instances calculated during the current run
|
|
616
788
|
"""
|
|
789
|
+
|
|
617
790
|
# count how many instances we had before we started
|
|
618
|
-
previous_properties_end = len(
|
|
791
|
+
previous_properties_end = len(self.property_instances)
|
|
619
792
|
|
|
620
|
-
|
|
793
|
+
# Set up the output directory
|
|
794
|
+
self._init_output_dir()
|
|
621
795
|
|
|
622
|
-
|
|
623
|
-
|
|
796
|
+
try:
|
|
797
|
+
# _setup is likely overridden by an derived class
|
|
798
|
+
self._setup(material, **kwargs)
|
|
624
799
|
|
|
625
|
-
|
|
626
|
-
|
|
800
|
+
# implemented by each individual Test Driver
|
|
801
|
+
self._calculate(**kwargs)
|
|
802
|
+
finally:
|
|
803
|
+
# Postprocess output directory for this invocation
|
|
804
|
+
self._archive_aux_files()
|
|
627
805
|
|
|
628
806
|
# The current invocation returns a Python list of dictionaries containing all
|
|
629
807
|
# properties computed during this run
|
|
630
|
-
return
|
|
808
|
+
return self.property_instances[previous_properties_end:]
|
|
631
809
|
|
|
632
810
|
def _add_property_instance(
|
|
633
811
|
self, property_name: str, disclaimer: Optional[str] = None
|
|
@@ -745,7 +923,7 @@ class KIMTestDriver(ABC):
|
|
|
745
923
|
|
|
746
924
|
input_name = filename_path.name
|
|
747
925
|
if add_instance_id:
|
|
748
|
-
current_instance_id = len(
|
|
926
|
+
current_instance_id = len(self.property_instances)
|
|
749
927
|
root, ext = os.path.splitext(input_name)
|
|
750
928
|
root = root + "-" + str(current_instance_id)
|
|
751
929
|
final_name = root + ext
|
|
@@ -758,10 +936,10 @@ class KIMTestDriver(ABC):
|
|
|
758
936
|
|
|
759
937
|
shutil.move(filename, final_path)
|
|
760
938
|
|
|
939
|
+
output_relpath = os.path.relpath(final_path, output_path)
|
|
761
940
|
# Filenames are reported relative to $CWD/output
|
|
762
|
-
self._add_key_to_current_property_instance(
|
|
763
|
-
|
|
764
|
-
)
|
|
941
|
+
self._add_key_to_current_property_instance(name, output_relpath)
|
|
942
|
+
self.__files_to_keep_in_output.append(output_relpath)
|
|
765
943
|
|
|
766
944
|
def _get_supported_lammps_atom_style(self) -> str:
|
|
767
945
|
"""
|
|
@@ -834,17 +1012,14 @@ class KIMTestDriver(ABC):
|
|
|
834
1012
|
os.makedirs(filename_parent, exist_ok=True)
|
|
835
1013
|
kim_property_dump(self._get_serialized_property_instances(), filename)
|
|
836
1014
|
if filename_parent != Path("output").resolve():
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
for file_in_property in instance[key]["source-value"]:
|
|
846
|
-
if file_in_output_name == file_in_property:
|
|
847
|
-
shutil.move(file_in_output, filename_parent)
|
|
1015
|
+
msg = (
|
|
1016
|
+
f"Writing properties .edn file to non-standard location {filename}. "
|
|
1017
|
+
"note that all other files remain in 'output' directory."
|
|
1018
|
+
)
|
|
1019
|
+
print(msg)
|
|
1020
|
+
logger.info(msg)
|
|
1021
|
+
else:
|
|
1022
|
+
self.__files_to_keep_in_output.append(os.path.relpath(filename, "output"))
|
|
848
1023
|
|
|
849
1024
|
def get_isolated_energy_per_atom(self, symbol: str) -> float:
|
|
850
1025
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kim-tools
|
|
3
|
-
Version: 0.3
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Base classes and helper routines for writing KIM Tests
|
|
5
5
|
Author-email: ilia Nikiforov <nikif002@umn.edu>, Ellad Tadmor <tadmor@umn.edu>, Claire Waters <bwaters@umn.edu>, "Daniel S. Karls" <karl0100umn@gmail.com>, Matt Bierbaum <matt.bierbaum@gmail.com>, Eric Fuemmeler <efuemmel@umn.edu>, Philipp Hoellmer <ph2484@nyu.edu>, Guanming Zhang <gz2241@nyu.edu>, Tom Egg <tje3676@nyu.edu>, Navaneeth Mohan <mohan227@umn.edu>
|
|
6
6
|
Maintainer-email: ilia Nikiforov <nikif002@umn.edu>
|
|
@@ -35,6 +35,7 @@ Dynamic: license-file
|
|
|
35
35
|
[](https://github.com/openkim/kim-tools/actions/workflows/testing.yml)
|
|
36
36
|
[](https://kim-tools.readthedocs.io/en/latest/)
|
|
37
37
|
[](https://pypi.org/project/kim-tools/)
|
|
38
|
+
[](https://codecov.io/gh/openkim/kim-tools)
|
|
38
39
|
|
|
39
40
|
KIMTestDriver and SingleCrystalTestDriver classes for creating OpenKIM Test Drivers, and helper routines for writing
|
|
40
41
|
KIM Tests and Verification Checks. Documentation at https://kim-tools.readthedocs.io.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
kim_tools/__init__.py,sha256=
|
|
2
|
-
kim_tools/kimunits.py,sha256=
|
|
1
|
+
kim_tools/__init__.py,sha256=Tw-dFo4IjQt_u8V_veHj6byvwl1KfmazTL1GdoWwcgM,433
|
|
2
|
+
kim_tools/kimunits.py,sha256=aCouh7z6fQDR1rcbRTO72l-_RuccuznrKrNF18F-cEQ,6076
|
|
3
3
|
kim_tools/aflow_util/__init__.py,sha256=lJnQ8fZCma80QVRQeKvY4MQ87oCWu-9KATV3dKJfpDc,80
|
|
4
|
-
kim_tools/aflow_util/core.py,sha256=
|
|
4
|
+
kim_tools/aflow_util/core.py,sha256=OmU4-m2jl81jfs0ni4bSECpC8GYt7-hEEH7cfKmrdoM,80886
|
|
5
5
|
kim_tools/aflow_util/aflow_prototype_encyclopedia/data/A108B24C11D24_cP334_222_h4i_i_bf_i-001/info.json,sha256=IsFiO9X2Ko7yoq2QkDurUVP7k1BE4WFgblu7oxl6iZs,2013
|
|
6
6
|
kim_tools/aflow_util/aflow_prototype_encyclopedia/data/A10B11_tI84_139_dehim_eh2n-001/info.json,sha256=f1EdtouuSL2y9NNw40Rvz2J9ZZcsqQBcyEmlHj6XoW8,1186
|
|
7
7
|
kim_tools/aflow_util/aflow_prototype_encyclopedia/data/A10B2C_hP39_171_5c_c_a-001/info.json,sha256=vD1xjZKWShL0E6XNsSlmIhilGcGNefl56oQDLQlHO1M,1596
|
|
@@ -2004,7 +2004,7 @@ kim_tools/aflow_util/aflow_prototype_encyclopedia/data/A_tP50_134_a2m2n-001/info
|
|
|
2004
2004
|
kim_tools/ase/__init__.py,sha256=1i6ko5tNr0VZC3T7hoEzq4fnSU0DdxNpxXcSaWMcJWc,76
|
|
2005
2005
|
kim_tools/ase/core.py,sha256=umJY0LV3_zrEZLOXAFoz6AFigxA69sAOBzAGoBTDzxA,34335
|
|
2006
2006
|
kim_tools/symmetry_util/__init__.py,sha256=uu-ZSUDUTe2P81rkAS3tXverx31s_uZ3wL4SD_dn5aI,86
|
|
2007
|
-
kim_tools/symmetry_util/core.py,sha256=
|
|
2007
|
+
kim_tools/symmetry_util/core.py,sha256=y9LGnUYxWE5e5KMxzM96t_UzrC-B_aT3XnntW8p_jRA,41392
|
|
2008
2008
|
kim_tools/symmetry_util/elasticity.py,sha256=VxJ8wUcsSOPyJ8id6OpKmVlRAbIUIWxtzYJtkvVJIVs,13328
|
|
2009
2009
|
kim_tools/symmetry_util/data/elast_cubic.svg,sha256=UpN4XcoLoOwv8a7KE0WyINkSH5AU2DPVZ08eGQf-Cds,8953
|
|
2010
2010
|
kim_tools/symmetry_util/data/elast_hexagonal.svg,sha256=wOkw5IO3fZy039tLHhvtwrkatVYs5XzigUormLKtRlw,8705
|
|
@@ -2023,11 +2023,11 @@ kim_tools/symmetry_util/data/wyck_pos_xform_under_normalizer.json,sha256=6g1YuYh
|
|
|
2023
2023
|
kim_tools/symmetry_util/data/wyckoff_multiplicities.json,sha256=qG2RPBd_-ejDIfz-E4ZhkHyRpIboxRy7oiXkdDf5Eg8,32270
|
|
2024
2024
|
kim_tools/symmetry_util/data/wyckoff_sets.json,sha256=f5ZpHKDHo6_JWki1b7KUGoYLlhU-44Qikw_-PtbLssw,9248
|
|
2025
2025
|
kim_tools/test_driver/__init__.py,sha256=KOiceeZNqkfrgZ66CiRiUdniceDrCmmDXQkOw0wXaCQ,92
|
|
2026
|
-
kim_tools/test_driver/core.py,sha256=
|
|
2026
|
+
kim_tools/test_driver/core.py,sha256=MtoJgGWPbqaMG1yOhNZd83Waw5qKwI4jJKkXfP1rMJc,108701
|
|
2027
2027
|
kim_tools/vc/__init__.py,sha256=zXjhxXCKVMLBMXXWYG3if7VOpBnsFrn_RjVpnohDm5c,74
|
|
2028
2028
|
kim_tools/vc/core.py,sha256=BIjzEExnQAL2S90a_npptRm3ACqAo4fZBtvTDBMWMdw,13963
|
|
2029
|
-
kim_tools-0.3.
|
|
2030
|
-
kim_tools-0.3.
|
|
2031
|
-
kim_tools-0.3.
|
|
2032
|
-
kim_tools-0.3.
|
|
2033
|
-
kim_tools-0.3.
|
|
2029
|
+
kim_tools-0.4.3.dist-info/licenses/LICENSE.CDDL,sha256=I2luEED_SHjuZ01B4rYG-AF_135amL24JpHvZ1Jhqe8,16373
|
|
2030
|
+
kim_tools-0.4.3.dist-info/METADATA,sha256=8bkatoFLO7BAGC-C6gGwIoCjI4s7HNqNLv-SmM10rZw,2196
|
|
2031
|
+
kim_tools-0.4.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
2032
|
+
kim_tools-0.4.3.dist-info/top_level.txt,sha256=w_YCpJ5ERigj9te74ln7k64tqj1VumOzM_s9dsalIWY,10
|
|
2033
|
+
kim_tools-0.4.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|