kim-tools 0.3.6__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 +81 -27
- kim_tools/ase/core.py +122 -33
- kim_tools/kimunits.py +26 -10
- kim_tools/symmetry_util/core.py +276 -102
- kim_tools/symmetry_util/data/elast_cubic.svg +105 -0
- kim_tools/symmetry_util/data/elast_hexagonal.svg +105 -0
- kim_tools/symmetry_util/data/elast_monoclinic.svg +108 -0
- kim_tools/symmetry_util/data/elast_orthorhombic.svg +96 -0
- kim_tools/symmetry_util/data/elast_tetragonal_4_slash_m.svg +114 -0
- kim_tools/symmetry_util/data/elast_tetragonal_4_slash_mmm.svg +105 -0
- kim_tools/symmetry_util/data/elast_triclinic.svg +132 -0
- kim_tools/symmetry_util/data/elast_trigonal_3bar.svg +129 -0
- kim_tools/symmetry_util/data/elast_trigonal_3bar_m_2nd_pos.svg +117 -0
- kim_tools/symmetry_util/data/elast_trigonal_3bar_m_3rd_pos.svg +117 -0
- kim_tools/symmetry_util/elasticity.py +390 -0
- kim_tools/test_driver/core.py +436 -80
- {kim_tools-0.3.6.dist-info → kim_tools-0.4.2.dist-info}/METADATA +3 -2
- {kim_tools-0.3.6.dist-info → kim_tools-0.4.2.dist-info}/RECORD +22 -11
- {kim_tools-0.3.6.dist-info → kim_tools-0.4.2.dist-info}/WHEEL +0 -0
- {kim_tools-0.3.6.dist-info → kim_tools-0.4.2.dist-info}/licenses/LICENSE.CDDL +0 -0
- {kim_tools-0.3.6.dist-info → kim_tools-0.4.2.dist-info}/top_level.txt +0 -0
kim_tools/test_driver/core.py
CHANGED
|
@@ -30,12 +30,16 @@
|
|
|
30
30
|
Helper classes for KIM Test Drivers
|
|
31
31
|
|
|
32
32
|
"""
|
|
33
|
+
import glob
|
|
34
|
+
import json
|
|
33
35
|
import logging
|
|
34
36
|
import os
|
|
35
37
|
import shutil
|
|
38
|
+
import tarfile
|
|
36
39
|
from abc import ABC, abstractmethod
|
|
37
40
|
from copy import deepcopy
|
|
38
41
|
from pathlib import Path
|
|
42
|
+
from secrets import token_bytes
|
|
39
43
|
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
|
40
44
|
from typing import IO, Any, Dict, List, Optional, Union
|
|
41
45
|
|
|
@@ -44,7 +48,9 @@ import kim_edn
|
|
|
44
48
|
import numpy as np
|
|
45
49
|
import numpy.typing as npt
|
|
46
50
|
from ase import Atoms
|
|
51
|
+
from ase.build import bulk
|
|
47
52
|
from ase.calculators.calculator import Calculator
|
|
53
|
+
from ase.calculators.kim import get_model_supported_species
|
|
48
54
|
from ase.constraints import FixSymmetry
|
|
49
55
|
from ase.data import atomic_masses
|
|
50
56
|
from ase.filters import FrechetCellFilter, UnitCellFilter
|
|
@@ -63,6 +69,7 @@ from kim_query import raw_query
|
|
|
63
69
|
from ..aflow_util import (
|
|
64
70
|
AFLOW,
|
|
65
71
|
get_space_group_number_from_prototype,
|
|
72
|
+
get_stoich_reduced_list_from_prototype,
|
|
66
73
|
prototype_labels_are_equivalent,
|
|
67
74
|
)
|
|
68
75
|
from ..aflow_util.core import AFLOW_EXECUTABLE, get_atom_indices_for_each_wyckoff_orb
|
|
@@ -91,6 +98,8 @@ __all__ = [
|
|
|
91
98
|
"detect_unique_crystal_structures",
|
|
92
99
|
"get_deduplicated_property_instances",
|
|
93
100
|
"minimize_wrapper",
|
|
101
|
+
"crystal_input_from_test_generator_line",
|
|
102
|
+
"get_supported_lammps_atom_style",
|
|
94
103
|
]
|
|
95
104
|
|
|
96
105
|
# Force tolerance for the optional initial relaxation of the provided cell
|
|
@@ -102,6 +111,50 @@ PROP_SEARCH_PATHS_INFO = (
|
|
|
102
111
|
"- $PWD/local-props/**/\n"
|
|
103
112
|
"- $PWD/local_props/**/"
|
|
104
113
|
)
|
|
114
|
+
TOKEN_NAME = "kim-tools.token"
|
|
115
|
+
TOKENPATH = os.path.join("output", TOKEN_NAME)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_supported_lammps_atom_style(model: str) -> str:
|
|
119
|
+
"""
|
|
120
|
+
Get the supported LAMMPS atom_style of a KIM model
|
|
121
|
+
"""
|
|
122
|
+
from lammps import lammps
|
|
123
|
+
|
|
124
|
+
candidate_atom_styles = ("atomic", "charge", "full")
|
|
125
|
+
banned_species = "electron" # Species in KIM models not understood by ASE
|
|
126
|
+
supported_species = get_model_supported_species(model)
|
|
127
|
+
test_species = None
|
|
128
|
+
for species in supported_species:
|
|
129
|
+
if species not in banned_species:
|
|
130
|
+
test_species = species
|
|
131
|
+
break
|
|
132
|
+
if test_species is None:
|
|
133
|
+
raise RuntimeError(
|
|
134
|
+
"Model appears to only support species not understood by ASE:\n"
|
|
135
|
+
+ str(supported_species)
|
|
136
|
+
)
|
|
137
|
+
atoms = bulk(test_species, "fcc", 10.0) # Very low-density FCC lattice
|
|
138
|
+
for atom_style in candidate_atom_styles:
|
|
139
|
+
with lammps(cmdargs=["-sc", "none"]) as lmp, NamedTemporaryFile() as f:
|
|
140
|
+
lmp.command(f"kim init {model} metal unit_conversion_mode")
|
|
141
|
+
atoms.write(f.name, format="lammps-data", atom_style=atom_style)
|
|
142
|
+
try:
|
|
143
|
+
lmp.command(f"read_data {f.name}")
|
|
144
|
+
return atom_style
|
|
145
|
+
except Exception as e:
|
|
146
|
+
if str(e).startswith(
|
|
147
|
+
"ERROR: Incorrect format in Atoms section of data file:"
|
|
148
|
+
):
|
|
149
|
+
continue
|
|
150
|
+
else:
|
|
151
|
+
msg = (
|
|
152
|
+
"The following unexpected exception was encountered when trying"
|
|
153
|
+
" to determine atom_style:\n" + repr(e)
|
|
154
|
+
)
|
|
155
|
+
print(msg)
|
|
156
|
+
raise e
|
|
157
|
+
raise RuntimeError("Unable to determine supported atom type")
|
|
105
158
|
|
|
106
159
|
|
|
107
160
|
def _get_optional_source_value(property_instance: Dict, key: str) -> Any:
|
|
@@ -137,8 +190,8 @@ def minimize_wrapper(
|
|
|
137
190
|
steps: int = MAXSTEPS_INITIAL,
|
|
138
191
|
variable_cell: bool = True,
|
|
139
192
|
logfile: Optional[Union[str, IO]] = "kim-tools.log",
|
|
140
|
-
algorithm: Optimizer = LBFGSLineSearch,
|
|
141
|
-
cell_filter: UnitCellFilter = FrechetCellFilter,
|
|
193
|
+
algorithm: type[Optimizer] = LBFGSLineSearch,
|
|
194
|
+
cell_filter: type[UnitCellFilter] = FrechetCellFilter,
|
|
142
195
|
fix_symmetry: Union[bool, FixSymmetry] = False,
|
|
143
196
|
opt_kwargs: Dict = {},
|
|
144
197
|
flt_kwargs: Dict = {},
|
|
@@ -189,12 +242,13 @@ def minimize_wrapper(
|
|
|
189
242
|
Returns:
|
|
190
243
|
Whether the minimization succeeded
|
|
191
244
|
"""
|
|
245
|
+
existing_constraints = atoms.constraints
|
|
192
246
|
if fix_symmetry is not False:
|
|
193
247
|
if fix_symmetry is True:
|
|
194
248
|
symmetry = FixSymmetry(atoms)
|
|
195
249
|
else:
|
|
196
250
|
symmetry = fix_symmetry
|
|
197
|
-
atoms.set_constraint(symmetry)
|
|
251
|
+
atoms.set_constraint([symmetry] + existing_constraints)
|
|
198
252
|
if variable_cell:
|
|
199
253
|
supercell_wrapped = cell_filter(atoms, **flt_kwargs)
|
|
200
254
|
opt = algorithm(supercell_wrapped, logfile=logfile, **opt_kwargs)
|
|
@@ -226,7 +280,7 @@ def minimize_wrapper(
|
|
|
226
280
|
+ " steps."
|
|
227
281
|
)
|
|
228
282
|
|
|
229
|
-
|
|
283
|
+
atoms.set_constraint(existing_constraints)
|
|
230
284
|
|
|
231
285
|
if minimization_stalled or iteration_limits_reached:
|
|
232
286
|
try:
|
|
@@ -240,7 +294,7 @@ def minimize_wrapper(
|
|
|
240
294
|
"trying to evaluate final forces and stress:"
|
|
241
295
|
)
|
|
242
296
|
logger.info(repr(e))
|
|
243
|
-
|
|
297
|
+
return False
|
|
244
298
|
else:
|
|
245
299
|
return True
|
|
246
300
|
|
|
@@ -345,8 +399,15 @@ def _add_property_instance(
|
|
|
345
399
|
property_name = path_str
|
|
346
400
|
found_custom_property = True
|
|
347
401
|
break
|
|
348
|
-
except Exception:
|
|
349
|
-
|
|
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)
|
|
350
411
|
|
|
351
412
|
if not found_custom_property:
|
|
352
413
|
raise KIMTestDriverError(
|
|
@@ -500,6 +561,14 @@ class KIMTestDriver(ABC):
|
|
|
500
561
|
__output_property_instances (str):
|
|
501
562
|
Property instances, possibly accumulated over multiple invocations of
|
|
502
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
|
|
503
572
|
"""
|
|
504
573
|
|
|
505
574
|
class NonKIMModelError(Exception):
|
|
@@ -509,11 +578,15 @@ class KIMTestDriver(ABC):
|
|
|
509
578
|
requires a KIM model (e.g. a LAMMPS TD) with a non-KIM Calculator
|
|
510
579
|
"""
|
|
511
580
|
|
|
512
|
-
def __init__(
|
|
581
|
+
def __init__(
|
|
582
|
+
self, model: Union[str, Calculator], suppr_sm_lmp_log: bool = False
|
|
583
|
+
) -> None:
|
|
513
584
|
"""
|
|
514
585
|
Args:
|
|
515
586
|
model:
|
|
516
587
|
ASE calculator or KIM model name to use
|
|
588
|
+
suppr_sm_lmp_log:
|
|
589
|
+
Suppress writing a lammps.log
|
|
517
590
|
"""
|
|
518
591
|
if isinstance(model, Calculator):
|
|
519
592
|
self.__calc = model
|
|
@@ -523,8 +596,13 @@ class KIMTestDriver(ABC):
|
|
|
523
596
|
|
|
524
597
|
self.__kim_model_name = model
|
|
525
598
|
self.__calc = KIM(self.__kim_model_name)
|
|
599
|
+
if suppr_sm_lmp_log:
|
|
600
|
+
if hasattr(self.__calc.parameters, "log_file"):
|
|
601
|
+
self.__calc.parameters.log_file = None
|
|
526
602
|
|
|
527
603
|
self.__output_property_instances = "[]"
|
|
604
|
+
self.__files_to_keep_in_output = []
|
|
605
|
+
self.__token = None
|
|
528
606
|
|
|
529
607
|
def _setup(self, material, **kwargs) -> None:
|
|
530
608
|
"""
|
|
@@ -532,6 +610,106 @@ class KIMTestDriver(ABC):
|
|
|
532
610
|
"""
|
|
533
611
|
pass
|
|
534
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
|
+
|
|
535
713
|
@abstractmethod
|
|
536
714
|
def _calculate(self, **kwargs) -> None:
|
|
537
715
|
"""
|
|
@@ -546,8 +724,10 @@ class KIMTestDriver(ABC):
|
|
|
546
724
|
|
|
547
725
|
* Run :func:`~KIMTestDriver._setup` (the base class provides a barebones
|
|
548
726
|
version, derived classes may override)
|
|
727
|
+
* Call :func:`~KIMTestDriver._init_output_dir`
|
|
549
728
|
* Call :func:`~KIMTestDriver._calculate` (implemented by each individual
|
|
550
729
|
Test Driver)
|
|
730
|
+
* Call :func:`~KIMTestDriver._archive_aux_files`
|
|
551
731
|
|
|
552
732
|
Args:
|
|
553
733
|
material:
|
|
@@ -557,8 +737,12 @@ class KIMTestDriver(ABC):
|
|
|
557
737
|
Returns:
|
|
558
738
|
The property instances calculated during the current run
|
|
559
739
|
"""
|
|
740
|
+
|
|
560
741
|
# count how many instances we had before we started
|
|
561
|
-
previous_properties_end = len(
|
|
742
|
+
previous_properties_end = len(self.property_instances)
|
|
743
|
+
|
|
744
|
+
# Set up the output directory
|
|
745
|
+
self._init_output_dir()
|
|
562
746
|
|
|
563
747
|
# _setup is likely overridden by an derived class
|
|
564
748
|
self._setup(material, **kwargs)
|
|
@@ -566,9 +750,12 @@ class KIMTestDriver(ABC):
|
|
|
566
750
|
# implemented by each individual Test Driver
|
|
567
751
|
self._calculate(**kwargs)
|
|
568
752
|
|
|
753
|
+
# Postprocess output directory for this invocation
|
|
754
|
+
self._archive_aux_files()
|
|
755
|
+
|
|
569
756
|
# The current invocation returns a Python list of dictionaries containing all
|
|
570
757
|
# properties computed during this run
|
|
571
|
-
return
|
|
758
|
+
return self.property_instances[previous_properties_end:]
|
|
572
759
|
|
|
573
760
|
def _add_property_instance(
|
|
574
761
|
self, property_name: str, disclaimer: Optional[str] = None
|
|
@@ -686,7 +873,7 @@ class KIMTestDriver(ABC):
|
|
|
686
873
|
|
|
687
874
|
input_name = filename_path.name
|
|
688
875
|
if add_instance_id:
|
|
689
|
-
current_instance_id = len(
|
|
876
|
+
current_instance_id = len(self.property_instances)
|
|
690
877
|
root, ext = os.path.splitext(input_name)
|
|
691
878
|
root = root + "-" + str(current_instance_id)
|
|
692
879
|
final_name = root + ext
|
|
@@ -699,10 +886,18 @@ class KIMTestDriver(ABC):
|
|
|
699
886
|
|
|
700
887
|
shutil.move(filename, final_path)
|
|
701
888
|
|
|
889
|
+
output_relpath = os.path.relpath(final_path, output_path)
|
|
702
890
|
# Filenames are reported relative to $CWD/output
|
|
703
|
-
self._add_key_to_current_property_instance(
|
|
704
|
-
|
|
705
|
-
|
|
891
|
+
self._add_key_to_current_property_instance(name, output_relpath)
|
|
892
|
+
self.__files_to_keep_in_output.append(output_relpath)
|
|
893
|
+
|
|
894
|
+
def _get_supported_lammps_atom_style(self) -> str:
|
|
895
|
+
"""
|
|
896
|
+
Return the atom_style that should be used when writing LAMMPS data files.
|
|
897
|
+
This atom_style will be compatible with the KIM model this object
|
|
898
|
+
was instantiated with.
|
|
899
|
+
"""
|
|
900
|
+
return get_supported_lammps_atom_style(self.kim_model_name)
|
|
706
901
|
|
|
707
902
|
@property
|
|
708
903
|
def kim_model_name(self) -> Optional[str]:
|
|
@@ -727,8 +922,20 @@ class KIMTestDriver(ABC):
|
|
|
727
922
|
@property
|
|
728
923
|
def _calc(self) -> Optional[Calculator]:
|
|
729
924
|
"""
|
|
730
|
-
Get the ASE calculator
|
|
925
|
+
Get the ASE calculator. Reinstantiate it if it's a KIM SM
|
|
731
926
|
"""
|
|
927
|
+
if self.__kim_model_name is not None:
|
|
928
|
+
reinst = False
|
|
929
|
+
if hasattr(self.__calc, "clean"):
|
|
930
|
+
self.__calc.clean()
|
|
931
|
+
reinst = True
|
|
932
|
+
if hasattr(self.__calc, "__del__"):
|
|
933
|
+
self.__calc.__del__()
|
|
934
|
+
reinst = True
|
|
935
|
+
if reinst:
|
|
936
|
+
from ase.calculators.kim.kim import KIM
|
|
937
|
+
|
|
938
|
+
self.__calc = KIM(self.__kim_model_name)
|
|
732
939
|
return self.__calc
|
|
733
940
|
|
|
734
941
|
def _get_serialized_property_instances(self) -> str:
|
|
@@ -746,17 +953,23 @@ class KIMTestDriver(ABC):
|
|
|
746
953
|
def write_property_instances_to_file(self, filename="output/results.edn") -> None:
|
|
747
954
|
"""
|
|
748
955
|
Write internal property instances (possibly accumulated over several calls to
|
|
749
|
-
the Test Driver) to a file at the requested path.
|
|
750
|
-
the same directory.
|
|
956
|
+
the Test Driver) to a file at the requested path.
|
|
751
957
|
|
|
752
958
|
Args:
|
|
753
959
|
filename: path to write the file
|
|
754
960
|
"""
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
961
|
+
filename_parent = Path(filename).parent.resolve()
|
|
962
|
+
os.makedirs(filename_parent, exist_ok=True)
|
|
963
|
+
kim_property_dump(self._get_serialized_property_instances(), filename)
|
|
964
|
+
if filename_parent != Path("output").resolve():
|
|
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"))
|
|
760
973
|
|
|
761
974
|
def get_isolated_energy_per_atom(self, symbol: str) -> float:
|
|
762
975
|
"""
|
|
@@ -791,6 +1004,7 @@ def _add_common_crystal_genome_keys_to_current_property_instance(
|
|
|
791
1004
|
temperature_unit: Optional[str] = "K",
|
|
792
1005
|
crystal_genome_source_structure_id: Optional[List[List[str]]] = None,
|
|
793
1006
|
aflow_executable: str = AFLOW_EXECUTABLE,
|
|
1007
|
+
omit_keys: Optional[List[str]] = None,
|
|
794
1008
|
) -> str:
|
|
795
1009
|
"""
|
|
796
1010
|
Write common Crystal Genome keys to the last element of ``property_instances``. See
|
|
@@ -804,19 +1018,26 @@ def _add_common_crystal_genome_keys_to_current_property_instance(
|
|
|
804
1018
|
The key will be added to the last dictionary in the list
|
|
805
1019
|
aflow_executable:
|
|
806
1020
|
Path to the AFLOW executable
|
|
1021
|
+
omit_keys:
|
|
1022
|
+
Which keys to omit writing
|
|
807
1023
|
|
|
808
1024
|
Returns:
|
|
809
1025
|
Updated EDN-serialized list of property instances
|
|
810
1026
|
"""
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
property_instances
|
|
819
|
-
|
|
1027
|
+
if omit_keys is None:
|
|
1028
|
+
omit_keys = []
|
|
1029
|
+
if "prototype-label" not in omit_keys:
|
|
1030
|
+
property_instances = _add_key_to_current_property_instance(
|
|
1031
|
+
property_instances, "prototype-label", prototype_label
|
|
1032
|
+
)
|
|
1033
|
+
if "stoichiometric-species" not in omit_keys:
|
|
1034
|
+
property_instances = _add_key_to_current_property_instance(
|
|
1035
|
+
property_instances, "stoichiometric-species", stoichiometric_species
|
|
1036
|
+
)
|
|
1037
|
+
if "a" not in omit_keys:
|
|
1038
|
+
property_instances = _add_key_to_current_property_instance(
|
|
1039
|
+
property_instances, "a", a, a_unit
|
|
1040
|
+
)
|
|
820
1041
|
|
|
821
1042
|
# get parameter names
|
|
822
1043
|
aflow = AFLOW(aflow_executable=aflow_executable)
|
|
@@ -833,24 +1054,28 @@ def _add_common_crystal_genome_keys_to_current_property_instance(
|
|
|
833
1054
|
"Incorrect number of parameter_values (i.e. dimensionless parameters "
|
|
834
1055
|
"besides a) for the provided prototype"
|
|
835
1056
|
)
|
|
836
|
-
|
|
837
|
-
property_instances
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1057
|
+
if "parameter-names" not in omit_keys:
|
|
1058
|
+
property_instances = _add_key_to_current_property_instance(
|
|
1059
|
+
property_instances, "parameter-names", aflow_parameter_names[1:]
|
|
1060
|
+
)
|
|
1061
|
+
if "parameter-values" not in omit_keys:
|
|
1062
|
+
property_instances = _add_key_to_current_property_instance(
|
|
1063
|
+
property_instances, "parameter-values", parameter_values
|
|
1064
|
+
)
|
|
842
1065
|
|
|
843
1066
|
if short_name is not None:
|
|
844
1067
|
if not isinstance(short_name, list):
|
|
845
1068
|
short_name = [short_name]
|
|
846
|
-
|
|
847
|
-
property_instances
|
|
848
|
-
|
|
1069
|
+
if "short-name" not in omit_keys:
|
|
1070
|
+
property_instances = _add_key_to_current_property_instance(
|
|
1071
|
+
property_instances, "short-name", short_name
|
|
1072
|
+
)
|
|
849
1073
|
|
|
850
1074
|
if library_prototype_label is not None:
|
|
851
|
-
|
|
852
|
-
property_instances
|
|
853
|
-
|
|
1075
|
+
if "library-prototype-label" not in omit_keys:
|
|
1076
|
+
property_instances = _add_key_to_current_property_instance(
|
|
1077
|
+
property_instances, "library-prototype-label", library_prototype_label
|
|
1078
|
+
)
|
|
854
1079
|
|
|
855
1080
|
if cell_cauchy_stress is not None:
|
|
856
1081
|
if len(cell_cauchy_stress) != 6:
|
|
@@ -859,27 +1084,30 @@ def _add_common_crystal_genome_keys_to_current_property_instance(
|
|
|
859
1084
|
"order [xx, yy, zz, yz, xz, xy]"
|
|
860
1085
|
)
|
|
861
1086
|
if cell_cauchy_stress_unit is None:
|
|
862
|
-
raise
|
|
863
|
-
|
|
864
|
-
property_instances
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1087
|
+
raise KIMTestDriverError("Please provide a `cell_cauchy_stress_unit`")
|
|
1088
|
+
if "cell-cauchy-stress" not in omit_keys:
|
|
1089
|
+
property_instances = _add_key_to_current_property_instance(
|
|
1090
|
+
property_instances,
|
|
1091
|
+
"cell-cauchy-stress",
|
|
1092
|
+
cell_cauchy_stress,
|
|
1093
|
+
cell_cauchy_stress_unit,
|
|
1094
|
+
)
|
|
869
1095
|
|
|
870
1096
|
if temperature is not None:
|
|
871
1097
|
if temperature_unit is None:
|
|
872
|
-
raise
|
|
873
|
-
|
|
874
|
-
property_instances
|
|
875
|
-
|
|
1098
|
+
raise KIMTestDriverError("Please provide a `temperature_unit`")
|
|
1099
|
+
if "temperature" not in omit_keys:
|
|
1100
|
+
property_instances = _add_key_to_current_property_instance(
|
|
1101
|
+
property_instances, "temperature", temperature, temperature_unit
|
|
1102
|
+
)
|
|
876
1103
|
|
|
877
1104
|
if crystal_genome_source_structure_id is not None:
|
|
878
|
-
|
|
879
|
-
property_instances
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1105
|
+
if "crystal-genome-source-structure-id" not in omit_keys:
|
|
1106
|
+
property_instances = _add_key_to_current_property_instance(
|
|
1107
|
+
property_instances,
|
|
1108
|
+
"crystal-genome-source-structure-id",
|
|
1109
|
+
crystal_genome_source_structure_id,
|
|
1110
|
+
)
|
|
883
1111
|
|
|
884
1112
|
return property_instances
|
|
885
1113
|
|
|
@@ -901,6 +1129,7 @@ def _add_property_instance_and_common_crystal_genome_keys(
|
|
|
901
1129
|
disclaimer: Optional[str] = None,
|
|
902
1130
|
property_instances: Optional[str] = None,
|
|
903
1131
|
aflow_executable: str = AFLOW_EXECUTABLE,
|
|
1132
|
+
omit_keys: Optional[List[str]] = None,
|
|
904
1133
|
) -> str:
|
|
905
1134
|
"""
|
|
906
1135
|
Initialize a new property instance to ``property_instances`` (an empty
|
|
@@ -923,6 +1152,8 @@ def _add_property_instance_and_common_crystal_genome_keys(
|
|
|
923
1152
|
A pre-existing EDN-serialized list of KIM Property instances to add to
|
|
924
1153
|
aflow_executable:
|
|
925
1154
|
Path to the AFLOW executable
|
|
1155
|
+
omit_keys:
|
|
1156
|
+
Which keys to omit writing
|
|
926
1157
|
|
|
927
1158
|
Returns:
|
|
928
1159
|
Updated EDN-serialized list of property instances
|
|
@@ -945,6 +1176,7 @@ def _add_property_instance_and_common_crystal_genome_keys(
|
|
|
945
1176
|
cell_cauchy_stress_unit=cell_cauchy_stress_unit,
|
|
946
1177
|
cell_cauchy_stress=cell_cauchy_stress,
|
|
947
1178
|
aflow_executable=aflow_executable,
|
|
1179
|
+
omit_keys=omit_keys,
|
|
948
1180
|
)
|
|
949
1181
|
|
|
950
1182
|
|
|
@@ -1193,17 +1425,22 @@ class SingleCrystalTestDriver(KIMTestDriver):
|
|
|
1193
1425
|
"""
|
|
1194
1426
|
|
|
1195
1427
|
def __init__(
|
|
1196
|
-
self,
|
|
1428
|
+
self,
|
|
1429
|
+
model: Union[str, Calculator],
|
|
1430
|
+
suppr_sm_lmp_log: bool = False,
|
|
1431
|
+
aflow_executable: str = AFLOW_EXECUTABLE,
|
|
1197
1432
|
) -> None:
|
|
1198
1433
|
"""
|
|
1199
1434
|
Args:
|
|
1200
1435
|
model:
|
|
1201
1436
|
ASE calculator or KIM model name to use
|
|
1437
|
+
suppr_sm_lmp_log:
|
|
1438
|
+
Suppress writing a lammps.log
|
|
1202
1439
|
aflow_executable:
|
|
1203
1440
|
Path to AFLOW executable
|
|
1204
1441
|
"""
|
|
1205
1442
|
self.aflow_executable = aflow_executable
|
|
1206
|
-
super().__init__(model)
|
|
1443
|
+
super().__init__(model, suppr_sm_lmp_log=suppr_sm_lmp_log)
|
|
1207
1444
|
|
|
1208
1445
|
def _setup(
|
|
1209
1446
|
self,
|
|
@@ -1273,17 +1510,28 @@ class SingleCrystalTestDriver(KIMTestDriver):
|
|
|
1273
1510
|
crystal_structure = get_crystal_structure_from_atoms(
|
|
1274
1511
|
atoms=material, aflow_executable=self.aflow_executable
|
|
1275
1512
|
)
|
|
1513
|
+
aflow = AFLOW()
|
|
1514
|
+
atoms_rebuilt = get_atoms_from_crystal_structure(crystal_structure)
|
|
1515
|
+
_, self.__input_rotation, _, _ = (
|
|
1516
|
+
aflow.get_basistransformation_rotation_originshift_atom_map_from_atoms(
|
|
1517
|
+
atoms_rebuilt,
|
|
1518
|
+
material,
|
|
1519
|
+
)
|
|
1520
|
+
)
|
|
1276
1521
|
msg = (
|
|
1277
|
-
"Rebuilding
|
|
1522
|
+
"Rebuilding Atoms object in a standard setting defined by "
|
|
1278
1523
|
"doi.org/10.1016/j.commatsci.2017.01.017. See log file or computed "
|
|
1279
1524
|
"properties for the (possibly re-oriented) primitive cell that "
|
|
1280
|
-
"computations will be based on."
|
|
1525
|
+
"computations will be based on. To obtain the rotation of this "
|
|
1526
|
+
"cell relative to the Atoms object you provided, use "
|
|
1527
|
+
f"{self.__class__.__name__}.get_input_rotation()"
|
|
1281
1528
|
)
|
|
1282
1529
|
logger.info(msg)
|
|
1283
1530
|
print()
|
|
1284
1531
|
print(msg)
|
|
1285
1532
|
print()
|
|
1286
1533
|
else:
|
|
1534
|
+
self.__input_rotation = None
|
|
1287
1535
|
crystal_structure = material
|
|
1288
1536
|
|
|
1289
1537
|
# Pop the temperature and stress keys in case they came along with a query
|
|
@@ -1337,6 +1585,7 @@ class SingleCrystalTestDriver(KIMTestDriver):
|
|
|
1337
1585
|
cell_rtol: float = 0.01,
|
|
1338
1586
|
rot_rtol: float = 0.01,
|
|
1339
1587
|
rot_atol: float = 0.01,
|
|
1588
|
+
match_library_proto: bool = True,
|
|
1340
1589
|
) -> None:
|
|
1341
1590
|
"""
|
|
1342
1591
|
Update the nominal parameter values of the nominal crystal structure from the
|
|
@@ -1381,6 +1630,8 @@ class SingleCrystalTestDriver(KIMTestDriver):
|
|
|
1381
1630
|
Parameter to pass to :func:`numpy.allclose` for compariong fractional
|
|
1382
1631
|
rotations. Default value chosen to be commensurate with AFLOW
|
|
1383
1632
|
default distance tolerance of 0.01*(NN distance)
|
|
1633
|
+
match_library_proto:
|
|
1634
|
+
Whether to match to library prototypes
|
|
1384
1635
|
|
|
1385
1636
|
Raises:
|
|
1386
1637
|
AFLOW.FailedToMatchException:
|
|
@@ -1393,18 +1644,29 @@ class SingleCrystalTestDriver(KIMTestDriver):
|
|
|
1393
1644
|
"""
|
|
1394
1645
|
aflow = AFLOW(aflow_executable=self.aflow_executable)
|
|
1395
1646
|
try:
|
|
1396
|
-
|
|
1397
|
-
|
|
1647
|
+
if match_library_proto:
|
|
1648
|
+
(aflow_parameter_values, library_prototype_label, short_name) = (
|
|
1649
|
+
aflow.solve_for_params_of_known_prototype(
|
|
1650
|
+
atoms=atoms,
|
|
1651
|
+
prototype_label=self.get_nominal_prototype_label(),
|
|
1652
|
+
max_resid=max_resid,
|
|
1653
|
+
cell_rtol=cell_rtol,
|
|
1654
|
+
rot_rtol=rot_rtol,
|
|
1655
|
+
rot_atol=rot_atol,
|
|
1656
|
+
)
|
|
1657
|
+
)
|
|
1658
|
+
else:
|
|
1659
|
+
aflow_parameter_values = aflow.solve_for_params_of_known_prototype(
|
|
1398
1660
|
atoms=atoms,
|
|
1399
|
-
prototype_label=self.
|
|
1400
|
-
"prototype-label"
|
|
1401
|
-
]["source-value"],
|
|
1661
|
+
prototype_label=self.get_nominal_prototype_label(),
|
|
1402
1662
|
max_resid=max_resid,
|
|
1403
1663
|
cell_rtol=cell_rtol,
|
|
1404
1664
|
rot_rtol=rot_rtol,
|
|
1405
1665
|
rot_atol=rot_atol,
|
|
1666
|
+
match_library_proto=False,
|
|
1406
1667
|
)
|
|
1407
|
-
|
|
1668
|
+
library_prototype_label = None
|
|
1669
|
+
short_name = None
|
|
1408
1670
|
except (AFLOW.FailedToMatchException, AFLOW.ChangedSymmetryException) as e:
|
|
1409
1671
|
raise type(e)(
|
|
1410
1672
|
"Encountered an error that MAY be the result of the nominal crystal "
|
|
@@ -1498,6 +1760,7 @@ class SingleCrystalTestDriver(KIMTestDriver):
|
|
|
1498
1760
|
stress_unit: Optional[str] = None,
|
|
1499
1761
|
temp_unit: str = "K",
|
|
1500
1762
|
disclaimer: Optional[str] = None,
|
|
1763
|
+
omit_keys: Optional[List[str]] = None,
|
|
1501
1764
|
) -> None:
|
|
1502
1765
|
"""
|
|
1503
1766
|
Initialize a new property instance to ``self.property_instances``. It will
|
|
@@ -1528,6 +1791,8 @@ class SingleCrystalTestDriver(KIMTestDriver):
|
|
|
1528
1791
|
disclaimer:
|
|
1529
1792
|
An optional disclaimer commenting on the applicability of this result,
|
|
1530
1793
|
e.g. "This relaxation did not reach the desired tolerance."
|
|
1794
|
+
omit_keys:
|
|
1795
|
+
Which keys to omit writing
|
|
1531
1796
|
"""
|
|
1532
1797
|
crystal_structure = self.__nominal_crystal_structure_npt
|
|
1533
1798
|
|
|
@@ -1626,17 +1891,22 @@ class SingleCrystalTestDriver(KIMTestDriver):
|
|
|
1626
1891
|
disclaimer=disclaimer,
|
|
1627
1892
|
property_instances=super()._get_serialized_property_instances(),
|
|
1628
1893
|
aflow_executable=self.aflow_executable,
|
|
1894
|
+
omit_keys=omit_keys,
|
|
1629
1895
|
)
|
|
1630
1896
|
)
|
|
1631
1897
|
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1898
|
+
if omit_keys is None:
|
|
1899
|
+
omit_keys = []
|
|
1900
|
+
if "coordinates-file" not in omit_keys:
|
|
1901
|
+
self.__add_poscar_to_curr_prop_inst(
|
|
1902
|
+
"primitive", "instance.poscar", "coordinates-file"
|
|
1903
|
+
)
|
|
1904
|
+
if "coordinates-file-conventional" not in omit_keys:
|
|
1905
|
+
self.__add_poscar_to_curr_prop_inst(
|
|
1906
|
+
"conventional",
|
|
1907
|
+
"conventional.instance.poscar",
|
|
1908
|
+
"coordinates-file-conventional",
|
|
1909
|
+
)
|
|
1640
1910
|
|
|
1641
1911
|
def _get_temperature(self, unit: str = "K") -> float:
|
|
1642
1912
|
"""
|
|
@@ -1872,18 +2142,65 @@ class SingleCrystalTestDriver(KIMTestDriver):
|
|
|
1872
2142
|
"source-value"
|
|
1873
2143
|
]
|
|
1874
2144
|
|
|
2145
|
+
def get_nominal_space_group_number(self) -> int:
|
|
2146
|
+
return get_space_group_number_from_prototype(self.get_nominal_prototype_label())
|
|
2147
|
+
|
|
2148
|
+
def get_nominal_stoichiometric_species(self) -> List[str]:
|
|
2149
|
+
return self._get_nominal_crystal_structure_npt()["stoichiometric-species"][
|
|
2150
|
+
"source-value"
|
|
2151
|
+
]
|
|
2152
|
+
|
|
2153
|
+
def get_nominal_stoichiometry(self) -> List[int]:
|
|
2154
|
+
return get_stoich_reduced_list_from_prototype(
|
|
2155
|
+
self.get_nominal_prototype_label()
|
|
2156
|
+
)
|
|
2157
|
+
|
|
2158
|
+
def get_nominal_a(self) -> float:
|
|
2159
|
+
return self._get_nominal_crystal_structure_npt()["a"]["source-value"]
|
|
2160
|
+
|
|
2161
|
+
def get_nominal_parameter_names(self) -> List[str]:
|
|
2162
|
+
return _get_optional_source_value(
|
|
2163
|
+
self._get_nominal_crystal_structure_npt(), "parameter-names"
|
|
2164
|
+
)
|
|
2165
|
+
|
|
2166
|
+
def get_nominal_parameter_values(self) -> List[float]:
|
|
2167
|
+
return _get_optional_source_value(
|
|
2168
|
+
self._get_nominal_crystal_structure_npt(), "parameter-values"
|
|
2169
|
+
)
|
|
2170
|
+
|
|
2171
|
+
def get_nominal_short_name(self) -> List[str]:
|
|
2172
|
+
return _get_optional_source_value(
|
|
2173
|
+
self._get_nominal_crystal_structure_npt(), "short-name"
|
|
2174
|
+
)
|
|
2175
|
+
|
|
2176
|
+
def get_nominal_library_prototype_label(self) -> str:
|
|
2177
|
+
return _get_optional_source_value(
|
|
2178
|
+
self._get_nominal_crystal_structure_npt(), "library-prototype-label"
|
|
2179
|
+
)
|
|
2180
|
+
|
|
1875
2181
|
def get_atom_indices_for_each_wyckoff_orb(self) -> List[Dict]:
|
|
1876
2182
|
"""
|
|
1877
2183
|
Get a list of dictionaries containing the atom indices of each Wyckoff
|
|
1878
2184
|
orbit.
|
|
1879
2185
|
|
|
1880
2186
|
Returns:
|
|
1881
|
-
The information is in this format
|
|
1882
|
-
|
|
1883
|
-
[{"letter":"a", "indices":[0,1]}, ... ]
|
|
2187
|
+
The information is in this format --
|
|
2188
|
+
``[{"letter":"a", "indices":[0,1]}, ... ]``
|
|
1884
2189
|
"""
|
|
1885
2190
|
return get_atom_indices_for_each_wyckoff_orb(self.get_nominal_prototype_label())
|
|
1886
2191
|
|
|
2192
|
+
def get_input_rotation(self) -> Optional[npt.ArrayLike]:
|
|
2193
|
+
"""
|
|
2194
|
+
Returns:
|
|
2195
|
+
If the Test Driver was called with an Atoms object, the nominal crystal
|
|
2196
|
+
structure may be rotated w.r.t. the input.
|
|
2197
|
+
This returns the Cartesian rotation to transform the Atoms input to the
|
|
2198
|
+
internal nominal crystal structure. I.e., if you want to get computed
|
|
2199
|
+
tensor properties in the same orientation as your input, you should
|
|
2200
|
+
rotate the reported tensors by the transpose of this rotation.
|
|
2201
|
+
"""
|
|
2202
|
+
return self.__input_rotation
|
|
2203
|
+
|
|
1887
2204
|
|
|
1888
2205
|
def query_crystal_structures(
|
|
1889
2206
|
stoichiometric_species: List[str],
|
|
@@ -2236,3 +2553,42 @@ def get_deduplicated_property_instances(
|
|
|
2236
2553
|
property_instances_deduplicated.sort(key=lambda a: a["instance-id"])
|
|
2237
2554
|
|
|
2238
2555
|
return property_instances_deduplicated
|
|
2556
|
+
|
|
2557
|
+
|
|
2558
|
+
def crystal_input_from_test_generator_line(
|
|
2559
|
+
test_generator_line: str, kim_model_name: str
|
|
2560
|
+
) -> List[Dict]:
|
|
2561
|
+
"""
|
|
2562
|
+
Produce a list of dictionaries of kwargs for a Crystal Genome Test Driver invocation
|
|
2563
|
+
from a line in its ``test_generator.json``
|
|
2564
|
+
"""
|
|
2565
|
+
test_generator_dict = json.loads(test_generator_line)
|
|
2566
|
+
stoichiometric_species = test_generator_dict["stoichiometric_species"]
|
|
2567
|
+
prototype_label = test_generator_dict["prototype_label"]
|
|
2568
|
+
cell_cauchy_stress_eV_angstrom3 = test_generator_dict.get(
|
|
2569
|
+
"cell_cauchy_stress_eV_angstrom3"
|
|
2570
|
+
)
|
|
2571
|
+
temperature_K = test_generator_dict.get("temperature_K")
|
|
2572
|
+
crystal_genome_test_args = test_generator_dict.get("crystal_genome_test_args")
|
|
2573
|
+
equilibria = query_crystal_structures(
|
|
2574
|
+
stoichiometric_species=stoichiometric_species,
|
|
2575
|
+
prototype_label=prototype_label,
|
|
2576
|
+
kim_model_name=kim_model_name,
|
|
2577
|
+
)
|
|
2578
|
+
inputs = []
|
|
2579
|
+
for equilibrium in equilibria:
|
|
2580
|
+
inputs.append(
|
|
2581
|
+
{
|
|
2582
|
+
"material": equilibrium,
|
|
2583
|
+
}
|
|
2584
|
+
)
|
|
2585
|
+
if cell_cauchy_stress_eV_angstrom3 is not None:
|
|
2586
|
+
inputs[-1][
|
|
2587
|
+
"cell_cauchy_stress_eV_angstrom3"
|
|
2588
|
+
] = cell_cauchy_stress_eV_angstrom3
|
|
2589
|
+
if temperature_K is not None:
|
|
2590
|
+
inputs[-1]["temperature_K"] = temperature_K
|
|
2591
|
+
if crystal_genome_test_args is not None:
|
|
2592
|
+
inputs[-1].update(crystal_genome_test_args)
|
|
2593
|
+
|
|
2594
|
+
return inputs
|