kim-tools 0.3.13__py3-none-any.whl → 0.4.2__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 +145 -20
- {kim_tools-0.3.13.dist-info → kim_tools-0.4.2.dist-info}/METADATA +2 -1
- {kim_tools-0.3.13.dist-info → kim_tools-0.4.2.dist-info}/RECORD +10 -10
- {kim_tools-0.3.13.dist-info → kim_tools-0.4.2.dist-info}/WHEEL +0 -0
- {kim_tools-0.3.13.dist-info → kim_tools-0.4.2.dist-info}/licenses/LICENSE.CDDL +0 -0
- {kim_tools-0.3.13.dist-info → kim_tools-0.4.2.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
|
@@ -35,9 +35,11 @@ import json
|
|
|
35
35
|
import logging
|
|
36
36
|
import os
|
|
37
37
|
import shutil
|
|
38
|
+
import tarfile
|
|
38
39
|
from abc import ABC, abstractmethod
|
|
39
40
|
from copy import deepcopy
|
|
40
41
|
from pathlib import Path
|
|
42
|
+
from secrets import token_bytes
|
|
41
43
|
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
|
42
44
|
from typing import IO, Any, Dict, List, Optional, Union
|
|
43
45
|
|
|
@@ -109,6 +111,8 @@ PROP_SEARCH_PATHS_INFO = (
|
|
|
109
111
|
"- $PWD/local-props/**/\n"
|
|
110
112
|
"- $PWD/local_props/**/"
|
|
111
113
|
)
|
|
114
|
+
TOKEN_NAME = "kim-tools.token"
|
|
115
|
+
TOKENPATH = os.path.join("output", TOKEN_NAME)
|
|
112
116
|
|
|
113
117
|
|
|
114
118
|
def get_supported_lammps_atom_style(model: str) -> str:
|
|
@@ -395,8 +399,15 @@ def _add_property_instance(
|
|
|
395
399
|
property_name = path_str
|
|
396
400
|
found_custom_property = True
|
|
397
401
|
break
|
|
398
|
-
except Exception:
|
|
399
|
-
|
|
402
|
+
except Exception as e:
|
|
403
|
+
msg = (
|
|
404
|
+
"MESSAGE: Trying to load a property from the .edn file at\n"
|
|
405
|
+
f"{path}\n"
|
|
406
|
+
"failed with the following exception:\n"
|
|
407
|
+
f"{repr(e)}"
|
|
408
|
+
)
|
|
409
|
+
logger.info(msg)
|
|
410
|
+
print(msg)
|
|
400
411
|
|
|
401
412
|
if not found_custom_property:
|
|
402
413
|
raise KIMTestDriverError(
|
|
@@ -550,6 +561,14 @@ class KIMTestDriver(ABC):
|
|
|
550
561
|
__output_property_instances (str):
|
|
551
562
|
Property instances, possibly accumulated over multiple invocations of
|
|
552
563
|
the Test Driver
|
|
564
|
+
__files_to_keep_in_output (List[PathLike]):
|
|
565
|
+
List of files that were written by this class explicitly, that we won't
|
|
566
|
+
touch when cleaning and backing up the output directory. Specified
|
|
567
|
+
relative to 'output' directory.
|
|
568
|
+
__token (Optional[bytes]):
|
|
569
|
+
Token that is written to TOKENPATH upon first evaluation. This
|
|
570
|
+
is used to check that multiple Test Drivers are not being called
|
|
571
|
+
concurrently, causing potential conflicts in the output directory
|
|
553
572
|
"""
|
|
554
573
|
|
|
555
574
|
class NonKIMModelError(Exception):
|
|
@@ -582,6 +601,8 @@ class KIMTestDriver(ABC):
|
|
|
582
601
|
self.__calc.parameters.log_file = None
|
|
583
602
|
|
|
584
603
|
self.__output_property_instances = "[]"
|
|
604
|
+
self.__files_to_keep_in_output = []
|
|
605
|
+
self.__token = None
|
|
585
606
|
|
|
586
607
|
def _setup(self, material, **kwargs) -> None:
|
|
587
608
|
"""
|
|
@@ -589,6 +610,106 @@ class KIMTestDriver(ABC):
|
|
|
589
610
|
"""
|
|
590
611
|
pass
|
|
591
612
|
|
|
613
|
+
def _init_output_dir(self) -> None:
|
|
614
|
+
"""
|
|
615
|
+
Initialize the output directory
|
|
616
|
+
"""
|
|
617
|
+
if self.__token is None:
|
|
618
|
+
# First time we've called this instance of the class
|
|
619
|
+
assert len(self.property_instances) == 0
|
|
620
|
+
|
|
621
|
+
os.makedirs("output", exist_ok=True)
|
|
622
|
+
|
|
623
|
+
# Move all top-level non-hidden files and directories
|
|
624
|
+
# to backup
|
|
625
|
+
output_glob = glob.glob("output/*")
|
|
626
|
+
if len(output_glob) > 0:
|
|
627
|
+
i = 0
|
|
628
|
+
while os.path.exists(f"output.{i}"):
|
|
629
|
+
i += 1
|
|
630
|
+
output_bak_name = f"output.{i}"
|
|
631
|
+
msg = (
|
|
632
|
+
"'output' directory is non-empty, backing up all "
|
|
633
|
+
f"non-hidden files and directories to {output_bak_name}"
|
|
634
|
+
)
|
|
635
|
+
print(msg)
|
|
636
|
+
logger.info(msg)
|
|
637
|
+
os.mkdir(output_bak_name)
|
|
638
|
+
for file_in_output in output_glob:
|
|
639
|
+
shutil.move(file_in_output, output_bak_name)
|
|
640
|
+
|
|
641
|
+
# Create token
|
|
642
|
+
self.__token = token_bytes(16)
|
|
643
|
+
with open(TOKENPATH, "wb") as f:
|
|
644
|
+
f.write(self.__token)
|
|
645
|
+
self.__files_to_keep_in_output.append(TOKEN_NAME)
|
|
646
|
+
else:
|
|
647
|
+
# Token is stored, check that it matches the token file
|
|
648
|
+
if not os.path.isfile(TOKENPATH):
|
|
649
|
+
raise KIMTestDriverError(
|
|
650
|
+
f"Token file at {TOKENPATH} was not found,"
|
|
651
|
+
"can't confirm non-interference of Test Drivers. Did something "
|
|
652
|
+
"edit the 'output' directory between calls to this Test Driver?"
|
|
653
|
+
)
|
|
654
|
+
else:
|
|
655
|
+
with open(TOKENPATH, "rb") as f:
|
|
656
|
+
if self.__token != f.read():
|
|
657
|
+
raise KIMTestDriverError(
|
|
658
|
+
f"Token file at {TOKENPATH} does not match this object's "
|
|
659
|
+
"token. This likely means that a different KIMTestDriver "
|
|
660
|
+
"instance was called between calls to this one. In order to"
|
|
661
|
+
" prevent conflicts in the output directory, this is not "
|
|
662
|
+
"allowed."
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
# We should have a record of all non-hidden files in output. If any
|
|
666
|
+
# untracked files are present, raise an error
|
|
667
|
+
output_glob = glob.glob("output/**", recursive=True)
|
|
668
|
+
for filepath in output_glob:
|
|
669
|
+
if os.path.isfile(filepath): # not tracking directories
|
|
670
|
+
if (
|
|
671
|
+
os.path.relpath(filepath, "output")
|
|
672
|
+
not in self.__files_to_keep_in_output
|
|
673
|
+
):
|
|
674
|
+
raise KIMTestDriverError(
|
|
675
|
+
f"Unknown file {filepath} in 'output' directory appeared "
|
|
676
|
+
"between calls to this Test Driver. This is not allowed "
|
|
677
|
+
"because stray files can cause issues."
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
def _archive_aux_files(self) -> None:
|
|
681
|
+
"""
|
|
682
|
+
Archive aux files after a run
|
|
683
|
+
"""
|
|
684
|
+
# Archive untracked files as aux files
|
|
685
|
+
i = 0
|
|
686
|
+
while f"aux_files.{i}.txz" in self.__files_to_keep_in_output:
|
|
687
|
+
assert os.path.isfile(f"output/aux_files.{i}.txz")
|
|
688
|
+
i += 1
|
|
689
|
+
tar_prefix = f"aux_files.{i}"
|
|
690
|
+
output_glob = glob.glob("output/**", recursive=True)
|
|
691
|
+
archived_files = [] # For deleting them later
|
|
692
|
+
with tarfile.open(f"output/{tar_prefix}.txz", "w:xz") as tar:
|
|
693
|
+
for filepath in output_glob:
|
|
694
|
+
if os.path.isfile(filepath): # not tracking directories
|
|
695
|
+
output_relpath = os.path.relpath(filepath, "output")
|
|
696
|
+
if output_relpath not in self.__files_to_keep_in_output:
|
|
697
|
+
tar.add(
|
|
698
|
+
os.path.join(filepath),
|
|
699
|
+
os.path.join(tar_prefix, output_relpath),
|
|
700
|
+
)
|
|
701
|
+
archived_files.append(filepath)
|
|
702
|
+
self.__files_to_keep_in_output.append(f"{tar_prefix}.txz")
|
|
703
|
+
for filepath in archived_files:
|
|
704
|
+
os.remove(filepath)
|
|
705
|
+
try:
|
|
706
|
+
os.removedirs(os.path.dirname(filepath))
|
|
707
|
+
except OSError:
|
|
708
|
+
pass # might not be empty yet
|
|
709
|
+
|
|
710
|
+
# should not have removed output dir in any situation
|
|
711
|
+
assert os.path.isdir("output")
|
|
712
|
+
|
|
592
713
|
@abstractmethod
|
|
593
714
|
def _calculate(self, **kwargs) -> None:
|
|
594
715
|
"""
|
|
@@ -603,8 +724,10 @@ class KIMTestDriver(ABC):
|
|
|
603
724
|
|
|
604
725
|
* Run :func:`~KIMTestDriver._setup` (the base class provides a barebones
|
|
605
726
|
version, derived classes may override)
|
|
727
|
+
* Call :func:`~KIMTestDriver._init_output_dir`
|
|
606
728
|
* Call :func:`~KIMTestDriver._calculate` (implemented by each individual
|
|
607
729
|
Test Driver)
|
|
730
|
+
* Call :func:`~KIMTestDriver._archive_aux_files`
|
|
608
731
|
|
|
609
732
|
Args:
|
|
610
733
|
material:
|
|
@@ -614,10 +737,12 @@ class KIMTestDriver(ABC):
|
|
|
614
737
|
Returns:
|
|
615
738
|
The property instances calculated during the current run
|
|
616
739
|
"""
|
|
740
|
+
|
|
617
741
|
# count how many instances we had before we started
|
|
618
|
-
previous_properties_end = len(
|
|
742
|
+
previous_properties_end = len(self.property_instances)
|
|
619
743
|
|
|
620
|
-
|
|
744
|
+
# Set up the output directory
|
|
745
|
+
self._init_output_dir()
|
|
621
746
|
|
|
622
747
|
# _setup is likely overridden by an derived class
|
|
623
748
|
self._setup(material, **kwargs)
|
|
@@ -625,9 +750,12 @@ class KIMTestDriver(ABC):
|
|
|
625
750
|
# implemented by each individual Test Driver
|
|
626
751
|
self._calculate(**kwargs)
|
|
627
752
|
|
|
753
|
+
# Postprocess output directory for this invocation
|
|
754
|
+
self._archive_aux_files()
|
|
755
|
+
|
|
628
756
|
# The current invocation returns a Python list of dictionaries containing all
|
|
629
757
|
# properties computed during this run
|
|
630
|
-
return
|
|
758
|
+
return self.property_instances[previous_properties_end:]
|
|
631
759
|
|
|
632
760
|
def _add_property_instance(
|
|
633
761
|
self, property_name: str, disclaimer: Optional[str] = None
|
|
@@ -745,7 +873,7 @@ class KIMTestDriver(ABC):
|
|
|
745
873
|
|
|
746
874
|
input_name = filename_path.name
|
|
747
875
|
if add_instance_id:
|
|
748
|
-
current_instance_id = len(
|
|
876
|
+
current_instance_id = len(self.property_instances)
|
|
749
877
|
root, ext = os.path.splitext(input_name)
|
|
750
878
|
root = root + "-" + str(current_instance_id)
|
|
751
879
|
final_name = root + ext
|
|
@@ -758,10 +886,10 @@ class KIMTestDriver(ABC):
|
|
|
758
886
|
|
|
759
887
|
shutil.move(filename, final_path)
|
|
760
888
|
|
|
889
|
+
output_relpath = os.path.relpath(final_path, output_path)
|
|
761
890
|
# Filenames are reported relative to $CWD/output
|
|
762
|
-
self._add_key_to_current_property_instance(
|
|
763
|
-
|
|
764
|
-
)
|
|
891
|
+
self._add_key_to_current_property_instance(name, output_relpath)
|
|
892
|
+
self.__files_to_keep_in_output.append(output_relpath)
|
|
765
893
|
|
|
766
894
|
def _get_supported_lammps_atom_style(self) -> str:
|
|
767
895
|
"""
|
|
@@ -834,17 +962,14 @@ class KIMTestDriver(ABC):
|
|
|
834
962
|
os.makedirs(filename_parent, exist_ok=True)
|
|
835
963
|
kim_property_dump(self._get_serialized_property_instances(), filename)
|
|
836
964
|
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)
|
|
965
|
+
msg = (
|
|
966
|
+
f"Writing properties .edn file to non-standard location {filename}. "
|
|
967
|
+
"note that all other files remain in 'output' directory."
|
|
968
|
+
)
|
|
969
|
+
print(msg)
|
|
970
|
+
logger.info(msg)
|
|
971
|
+
else:
|
|
972
|
+
self.__files_to_keep_in_output.append(os.path.relpath(filename, "output"))
|
|
848
973
|
|
|
849
974
|
def get_isolated_energy_per_atom(self, symbol: str) -> float:
|
|
850
975
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kim-tools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.2
|
|
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=ATvBQMEr5NBWxGzIDxLFJxMZIXTwxIx5RVS5-OO4H98,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=ugPL_wDzKwT7ch7Xh6SFMCLTF1Qf8x405HqcGJ29fd4,106587
|
|
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.
|
|
2030
|
-
kim_tools-0.
|
|
2031
|
-
kim_tools-0.
|
|
2032
|
-
kim_tools-0.
|
|
2033
|
-
kim_tools-0.
|
|
2029
|
+
kim_tools-0.4.2.dist-info/licenses/LICENSE.CDDL,sha256=I2luEED_SHjuZ01B4rYG-AF_135amL24JpHvZ1Jhqe8,16373
|
|
2030
|
+
kim_tools-0.4.2.dist-info/METADATA,sha256=dCmTQkNmZhR6y-2GtlwbgMnFMh14kgKgTlkvGL4UHpw,2196
|
|
2031
|
+
kim_tools-0.4.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
2032
|
+
kim_tools-0.4.2.dist-info/top_level.txt,sha256=w_YCpJ5ERigj9te74ln7k64tqj1VumOzM_s9dsalIWY,10
|
|
2033
|
+
kim_tools-0.4.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|