kim-tools 0.2.0b0__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 +14 -0
- kim_tools/aflow_util/__init__.py +4 -0
- kim_tools/aflow_util/core.py +1782 -0
- kim_tools/aflow_util/data/README_PROTO.TXT +3241 -0
- kim_tools/ase/__init__.py +4 -0
- kim_tools/ase/core.py +749 -0
- kim_tools/kimunits.py +162 -0
- kim_tools/symmetry_util/__init__.py +4 -0
- kim_tools/symmetry_util/core.py +552 -0
- kim_tools/symmetry_util/data/possible_primitive_shifts.json +1 -0
- kim_tools/symmetry_util/data/primitive_GENPOS_ops.json +1 -0
- kim_tools/symmetry_util/data/space_groups_for_each_bravais_lattice.json +179 -0
- kim_tools/symmetry_util/data/wyck_pos_xform_under_normalizer.json +1344 -0
- kim_tools/symmetry_util/data/wyckoff_multiplicities.json +2193 -0
- kim_tools/symmetry_util/data/wyckoff_sets.json +232 -0
- kim_tools/test_driver/__init__.py +4 -0
- kim_tools/test_driver/core.py +1932 -0
- kim_tools/vc/__init__.py +4 -0
- kim_tools/vc/core.py +397 -0
- kim_tools-0.2.0b0.dist-info/METADATA +32 -0
- kim_tools-0.2.0b0.dist-info/RECORD +24 -0
- kim_tools-0.2.0b0.dist-info/WHEEL +5 -0
- kim_tools-0.2.0b0.dist-info/licenses/LICENSE.CDDL +380 -0
- kim_tools-0.2.0b0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1932 @@
|
|
1
|
+
################################################################################
|
2
|
+
#
|
3
|
+
# CDDL HEADER START
|
4
|
+
#
|
5
|
+
# The contents of this file are subject to the terms of the Common Development
|
6
|
+
# and Distribution License Version 1.0 (the "License").
|
7
|
+
#
|
8
|
+
# You can obtain a copy of the license at
|
9
|
+
# http:# www.opensource.org/licenses/CDDL-1.0. See the License for the
|
10
|
+
# specific language governing permissions and limitations under the License.
|
11
|
+
#
|
12
|
+
# When distributing Covered Code, include this CDDL HEADER in each file and
|
13
|
+
# include the License file in a prominent location with the name LICENSE.CDDL.
|
14
|
+
# If applicable, add the following below this CDDL HEADER, with the fields
|
15
|
+
# enclosed by brackets "[]" replaced with your own identifying information:
|
16
|
+
#
|
17
|
+
# Portions Copyright (c) [yyyy] [name of copyright owner]. All rights reserved.
|
18
|
+
#
|
19
|
+
# CDDL HEADER END
|
20
|
+
#
|
21
|
+
# Copyright (c) 2017-2019, Regents of the University of Minnesota.
|
22
|
+
# All rights reserved.
|
23
|
+
#
|
24
|
+
# Contributor(s):
|
25
|
+
# Ilia Nikiforov
|
26
|
+
# Eric Fuemmler
|
27
|
+
#
|
28
|
+
################################################################################
|
29
|
+
"""
|
30
|
+
Helper classes for KIM Test Drivers
|
31
|
+
|
32
|
+
"""
|
33
|
+
import logging
|
34
|
+
import os
|
35
|
+
import shutil
|
36
|
+
from abc import ABC, abstractmethod
|
37
|
+
from copy import deepcopy
|
38
|
+
from pathlib import Path
|
39
|
+
from tempfile import NamedTemporaryFile, TemporaryDirectory
|
40
|
+
from typing import IO, Any, Dict, List, Optional, Union
|
41
|
+
|
42
|
+
import ase
|
43
|
+
import kim_edn
|
44
|
+
import numpy as np
|
45
|
+
from ase import Atoms
|
46
|
+
from ase.calculators.calculator import Calculator
|
47
|
+
from ase.constraints import FixSymmetry
|
48
|
+
from ase.data import atomic_masses
|
49
|
+
from ase.filters import FrechetCellFilter, UnitCellFilter
|
50
|
+
from ase.optimize import LBFGSLineSearch
|
51
|
+
from ase.optimize.optimize import Optimizer
|
52
|
+
from kim_property import (
|
53
|
+
get_properties,
|
54
|
+
get_property_id_path,
|
55
|
+
kim_property_create,
|
56
|
+
kim_property_dump,
|
57
|
+
kim_property_modify,
|
58
|
+
)
|
59
|
+
from kim_property.modify import STANDARD_KEYS_SCLAR_OR_WITH_EXTENT
|
60
|
+
from kim_query import raw_query
|
61
|
+
from numpy.typing import ArrayLike
|
62
|
+
|
63
|
+
from ..aflow_util import (
|
64
|
+
AFLOW,
|
65
|
+
get_space_group_number_from_prototype,
|
66
|
+
prototype_labels_are_equivalent,
|
67
|
+
)
|
68
|
+
from ..kimunits import convert_list, convert_units
|
69
|
+
from ..symmetry_util import (
|
70
|
+
cartesian_rotation_is_in_point_group,
|
71
|
+
change_of_basis_atoms,
|
72
|
+
get_cell_from_poscar,
|
73
|
+
get_change_of_basis_matrix_to_conventional_cell_from_formal_bravais_lattice,
|
74
|
+
get_formal_bravais_lattice_from_space_group,
|
75
|
+
)
|
76
|
+
|
77
|
+
logger = logging.getLogger(__name__)
|
78
|
+
logging.basicConfig(filename="kim-tools.log", level=logging.INFO, force=True)
|
79
|
+
|
80
|
+
__author__ = ["ilia Nikiforov", "Eric Fuemmeler"]
|
81
|
+
__all__ = [
|
82
|
+
"KIMTestDriverError",
|
83
|
+
"KIMTestDriver",
|
84
|
+
"get_crystal_structure_from_atoms",
|
85
|
+
"get_poscar_from_crystal_structure",
|
86
|
+
"get_atoms_from_crystal_structure",
|
87
|
+
"SingleCrystalTestDriver",
|
88
|
+
"query_crystal_structures",
|
89
|
+
"detect_unique_crystal_structures",
|
90
|
+
"get_deduplicated_property_instances",
|
91
|
+
"minimize_wrapper",
|
92
|
+
]
|
93
|
+
|
94
|
+
# Force tolerance for the optional initial relaxation of the provided cell
|
95
|
+
FMAX_INITIAL = 1e-5
|
96
|
+
# Maximum steps for the optional initial relaxation of the provided cell
|
97
|
+
MAXSTEPS_INITIAL = 200
|
98
|
+
PROP_SEARCH_PATHS_INFO = (
|
99
|
+
"- $KIM_PROPERTY_PATH (expanding globs including recursive **)\n"
|
100
|
+
"- $PWD/local-props/**/\n"
|
101
|
+
"- $PWD/local_props/**/"
|
102
|
+
)
|
103
|
+
|
104
|
+
|
105
|
+
def _get_optional_source_value(property_instance: Dict, key: str) -> Any:
|
106
|
+
"""
|
107
|
+
Function for getting an optional property key's source-value, or None if the key
|
108
|
+
doesn't exist
|
109
|
+
"""
|
110
|
+
if key in property_instance:
|
111
|
+
return property_instance[key]["source-value"]
|
112
|
+
else:
|
113
|
+
return None
|
114
|
+
|
115
|
+
|
116
|
+
def minimize_wrapper(
|
117
|
+
atoms: Atoms,
|
118
|
+
fmax: float = FMAX_INITIAL,
|
119
|
+
steps: int = MAXSTEPS_INITIAL,
|
120
|
+
variable_cell: bool = True,
|
121
|
+
logfile: Optional[Union[str, IO]] = "kim-tools.log",
|
122
|
+
algorithm: Optimizer = LBFGSLineSearch,
|
123
|
+
cell_filter: UnitCellFilter = FrechetCellFilter,
|
124
|
+
fix_symmetry: bool = False,
|
125
|
+
opt_kwargs: Dict = {},
|
126
|
+
flt_kwargs: Dict = {},
|
127
|
+
) -> bool:
|
128
|
+
"""
|
129
|
+
Use LBFGSLineSearch (default) to Minimize cell energy with respect to cell shape and
|
130
|
+
internal atom positions.
|
131
|
+
|
132
|
+
LBFGSLineSearch convergence behavior is as follows:
|
133
|
+
|
134
|
+
- The solver returns True if it is able to converge within the optimizer
|
135
|
+
iteration limits (which can be changed by the ``steps`` argument passed
|
136
|
+
to ``run``), otherwise it returns False.
|
137
|
+
- The solver raises an exception in situations where the line search cannot
|
138
|
+
improve the solution, typically due to an incompatibility between the
|
139
|
+
potential's values for energy, forces, and/or stress.
|
140
|
+
|
141
|
+
This routine attempts to minimizes the energy until the force and stress
|
142
|
+
reduce below specified tolerances given a provided limit on the number of
|
143
|
+
allowed steps. The code returns when convergence is achieved or no
|
144
|
+
further progress can be made, either due to reaching the iteration step
|
145
|
+
limit, or a stalled minimization due to line search failures.
|
146
|
+
|
147
|
+
Args:
|
148
|
+
atoms:
|
149
|
+
Atomic configuration to be minimized.
|
150
|
+
fmax:
|
151
|
+
Force convergence tolerance (the magnitude of the force on each
|
152
|
+
atom must be less than this for convergence)
|
153
|
+
steps:
|
154
|
+
Maximum number of iterations for the minimization
|
155
|
+
variable_cell:
|
156
|
+
True to allow relaxation with respect to cell shape
|
157
|
+
logfile:
|
158
|
+
Log file. ``'-'`` means STDOUT
|
159
|
+
algorithm:
|
160
|
+
ASE optimizer algorithm
|
161
|
+
CellFilter:
|
162
|
+
Filter to use if variable_cell is requested
|
163
|
+
fix_symmetry:
|
164
|
+
Whether to fix the crystallographic symmetry
|
165
|
+
opt_kwargs:
|
166
|
+
Dictionary of kwargs to pass to optimizer
|
167
|
+
flt_kwargs:
|
168
|
+
Dictionary of kwargs to pass to filter (e.g. "scalar_pressure")
|
169
|
+
|
170
|
+
Returns:
|
171
|
+
Whether the minimization succeeded
|
172
|
+
"""
|
173
|
+
if fix_symmetry:
|
174
|
+
symmetry = FixSymmetry(atoms)
|
175
|
+
atoms.set_constraint(symmetry)
|
176
|
+
if variable_cell:
|
177
|
+
supercell_wrapped = cell_filter(atoms, **flt_kwargs)
|
178
|
+
opt = algorithm(supercell_wrapped, logfile=logfile, **opt_kwargs)
|
179
|
+
else:
|
180
|
+
opt = algorithm(atoms, logfile=logfile, **opt_kwargs)
|
181
|
+
try:
|
182
|
+
converged = opt.run(fmax=fmax, steps=steps)
|
183
|
+
iteration_limits_reached = not converged
|
184
|
+
minimization_stalled = False
|
185
|
+
except Exception as e:
|
186
|
+
minimization_stalled = True
|
187
|
+
iteration_limits_reached = False
|
188
|
+
logger.info("The following exception was caught during minimization:")
|
189
|
+
logger.info(repr(e))
|
190
|
+
|
191
|
+
logger.info(
|
192
|
+
"Minimization "
|
193
|
+
+ (
|
194
|
+
"stalled"
|
195
|
+
if minimization_stalled
|
196
|
+
else "stopped" if iteration_limits_reached else "converged"
|
197
|
+
)
|
198
|
+
+ " after "
|
199
|
+
+ (
|
200
|
+
("hitting the maximum of " + str(steps))
|
201
|
+
if iteration_limits_reached
|
202
|
+
else str(opt.nsteps)
|
203
|
+
)
|
204
|
+
+ " steps."
|
205
|
+
)
|
206
|
+
|
207
|
+
del atoms.constraints
|
208
|
+
|
209
|
+
if minimization_stalled or iteration_limits_reached:
|
210
|
+
logger.info("Final forces:")
|
211
|
+
logger.info(atoms.get_forces())
|
212
|
+
logger.info("Final stress:")
|
213
|
+
logger.info(atoms.get_stress())
|
214
|
+
return False
|
215
|
+
else:
|
216
|
+
return True
|
217
|
+
|
218
|
+
|
219
|
+
def _add_property_instance(
|
220
|
+
property_name: str,
|
221
|
+
disclaimer: Optional[str] = None,
|
222
|
+
property_instances: Optional[str] = None,
|
223
|
+
) -> str:
|
224
|
+
"""
|
225
|
+
Initialize a new property instance in the serialized ``property_instances``. This
|
226
|
+
wraps the ``kim_property.kim_property_create()`` function, with the simplification
|
227
|
+
of setting "instance-id" automatically, as well as automatically searching for a
|
228
|
+
definition file if ``property_name`` is not found.
|
229
|
+
|
230
|
+
NOTE: Is there any need to allow Test Driver authors to specify an instance-id?
|
231
|
+
|
232
|
+
Args:
|
233
|
+
property_name:
|
234
|
+
The property name, e.g.
|
235
|
+
"tag:staff@noreply.openkim.org,2023-02-21:property/binding-energy-crystal"
|
236
|
+
or "binding-energy-crystal"
|
237
|
+
disclaimer:
|
238
|
+
An optional disclaimer commenting on the applicability of this result, e.g.
|
239
|
+
"This relaxation did not reach the desired tolerance."
|
240
|
+
property_instances:
|
241
|
+
A pre-existing EDN-serialized list of KIM Property instances to add to
|
242
|
+
|
243
|
+
Returns:
|
244
|
+
Updated EDN-serialized list of property instances
|
245
|
+
"""
|
246
|
+
if property_instances is None:
|
247
|
+
property_instances = "[]"
|
248
|
+
# Get and check the instance-id to use.
|
249
|
+
property_instances_deserialized = kim_edn.loads(property_instances)
|
250
|
+
new_instance_index = len(property_instances_deserialized) + 1
|
251
|
+
for property_instance in property_instances_deserialized:
|
252
|
+
assert (
|
253
|
+
property_instance["instance-id"] != new_instance_index
|
254
|
+
), "instance-id conflict"
|
255
|
+
|
256
|
+
existing_properties = get_properties()
|
257
|
+
property_in_existing_properties = False
|
258
|
+
for existing_property in existing_properties:
|
259
|
+
if (
|
260
|
+
existing_property == property_name
|
261
|
+
or get_property_id_path(existing_property)[3] == property_name
|
262
|
+
):
|
263
|
+
property_in_existing_properties = True
|
264
|
+
|
265
|
+
if not property_in_existing_properties:
|
266
|
+
print(
|
267
|
+
f"\nThe property name or id\n{property_name}\nwas not found in "
|
268
|
+
"kim-properties.\n"
|
269
|
+
)
|
270
|
+
print(
|
271
|
+
"I will now look for an .edn file containing its definition in the "
|
272
|
+
f"following locations:\n{PROP_SEARCH_PATHS_INFO}\n"
|
273
|
+
)
|
274
|
+
|
275
|
+
property_search_paths = []
|
276
|
+
|
277
|
+
# environment varible
|
278
|
+
if "KIM_PROPERTY_PATH" in os.environ:
|
279
|
+
property_search_paths += os.environ["KIM_PROPERTY_PATH"].split(":")
|
280
|
+
|
281
|
+
# CWD
|
282
|
+
property_search_paths.append(os.path.join(Path.cwd(), "local_props", "**"))
|
283
|
+
property_search_paths.append(os.path.join(Path.cwd(), "local-props", "**"))
|
284
|
+
|
285
|
+
# recursively search for .edn files in the paths, check if they are a property
|
286
|
+
# definition with the correct name
|
287
|
+
|
288
|
+
found_custom_property = False
|
289
|
+
|
290
|
+
for search_path in property_search_paths:
|
291
|
+
if found_custom_property:
|
292
|
+
break
|
293
|
+
else:
|
294
|
+
# hack to expand globs in both absolute and relative paths
|
295
|
+
if search_path[0] == "/":
|
296
|
+
base_path = Path("/")
|
297
|
+
search_glob = os.path.join(search_path[1:], "*.edn")
|
298
|
+
else:
|
299
|
+
base_path = Path()
|
300
|
+
search_glob = os.path.join(search_path, "*.edn")
|
301
|
+
|
302
|
+
for path in base_path.glob(search_glob):
|
303
|
+
if not os.path.isfile(
|
304
|
+
path
|
305
|
+
): # in case there's a directory named *.edn
|
306
|
+
continue
|
307
|
+
try:
|
308
|
+
path_str = str(path)
|
309
|
+
dict_from_edn = kim_edn.load(path_str)
|
310
|
+
if ("property-id") in dict_from_edn:
|
311
|
+
property_id = dict_from_edn["property-id"]
|
312
|
+
if (
|
313
|
+
property_id == property_name
|
314
|
+
or get_property_id_path(property_id)[3] == property_name
|
315
|
+
):
|
316
|
+
property_name = path_str
|
317
|
+
found_custom_property = True
|
318
|
+
break
|
319
|
+
except Exception:
|
320
|
+
pass
|
321
|
+
|
322
|
+
if not found_custom_property:
|
323
|
+
raise KIMTestDriverError(
|
324
|
+
f"\nThe property name or id\n{property_name}\nwas not found in "
|
325
|
+
"kim-properties.\nI failed to find an .edn file containing a matching "
|
326
|
+
'"property-id" key in the following locations:\n'
|
327
|
+
+ PROP_SEARCH_PATHS_INFO
|
328
|
+
)
|
329
|
+
|
330
|
+
return kim_property_create(
|
331
|
+
new_instance_index, property_name, property_instances, disclaimer
|
332
|
+
)
|
333
|
+
|
334
|
+
|
335
|
+
def _add_key_to_current_property_instance(
|
336
|
+
property_instances: str,
|
337
|
+
name: str,
|
338
|
+
value: ArrayLike,
|
339
|
+
unit: Optional[str] = None,
|
340
|
+
uncertainty_info: Optional[dict] = None,
|
341
|
+
) -> str:
|
342
|
+
"""
|
343
|
+
Write a key to the last element of property_instances. This wraps
|
344
|
+
``kim_property.kim_property_modify()`` with a simplified (and more restricted)
|
345
|
+
interface.
|
346
|
+
|
347
|
+
Note: if the value is an array, this function will assume you want to write to the
|
348
|
+
beginning of the array in every dimension.
|
349
|
+
|
350
|
+
This function is intended to write entire keys in one go, and should not be used for
|
351
|
+
modifying existing keys.
|
352
|
+
|
353
|
+
WARNING! It is the developer's responsibility to make sure the array shape matches
|
354
|
+
the extent specified in the property definition. This method uses fills the values
|
355
|
+
of array keys as slices through the last dimension. If those slices are incomplete,
|
356
|
+
kim_property automatically initializes the other elements in that slice to zero. For
|
357
|
+
example, consider writing coordinates to a key with extent [":", 3]. The correct way
|
358
|
+
to write a single atom would be to provide [[x, y, z]]. If you accidentally provide
|
359
|
+
[[x], [y], [z]], it will fill the field with the coordinates
|
360
|
+
[[x, 0, 0], [y, 0, 0], [z, 0, 0]]. This will not raise an error, only exceeding
|
361
|
+
the allowed dimesnsions of the key will do so.
|
362
|
+
|
363
|
+
Args:
|
364
|
+
property_instances:
|
365
|
+
An EDN-serialized list of dictionaries representing KIM Property Instances.
|
366
|
+
The key will be added to the last dictionary in the list
|
367
|
+
name:
|
368
|
+
Name of the key, e.g. "cell-cauchy-stress"
|
369
|
+
value:
|
370
|
+
The value of the key. The function will attempt to convert it to a NumPy
|
371
|
+
array, then use the dimensions of the resulting array. Scalars, lists,
|
372
|
+
tuples, and arrays should work.
|
373
|
+
Data type of the elements should be str, float, or int
|
374
|
+
unit:
|
375
|
+
The units
|
376
|
+
uncertainty_info:
|
377
|
+
dictionary containing any uncertainty keys you wish to include. See
|
378
|
+
https://openkim.org/doc/schema/properties-framework/ for the possible
|
379
|
+
uncertainty key names. These must be the same dimension as ``value``, or
|
380
|
+
they may be scalars regardless of the shape of ``value``.
|
381
|
+
|
382
|
+
Returns:
|
383
|
+
Updated EDN-serialized list of property instances
|
384
|
+
"""
|
385
|
+
|
386
|
+
def recur_dimensions(
|
387
|
+
prev_indices: List[int],
|
388
|
+
sub_value: np.ndarray,
|
389
|
+
modify_args: list,
|
390
|
+
key_name: str = "source-value",
|
391
|
+
):
|
392
|
+
sub_shape = sub_value.shape
|
393
|
+
assert (
|
394
|
+
len(sub_shape) != 0
|
395
|
+
), "Should not have gotten to zero dimensions in the recursive function"
|
396
|
+
if len(sub_shape) == 1:
|
397
|
+
# only if we have gotten to a 1-dimensional sub-array do we write stuff
|
398
|
+
modify_args += [key_name, *prev_indices, "1:%d" % sub_shape[0], *sub_value]
|
399
|
+
else:
|
400
|
+
for i in range(sub_shape[0]):
|
401
|
+
prev_indices.append(i + 1) # convert to 1-based indices
|
402
|
+
recur_dimensions(prev_indices, sub_value[i], modify_args, key_name)
|
403
|
+
prev_indices.pop()
|
404
|
+
|
405
|
+
value_arr = np.array(value)
|
406
|
+
value_shape = value_arr.shape
|
407
|
+
|
408
|
+
current_instance_index = len(kim_edn.loads(property_instances))
|
409
|
+
modify_args = ["key", name]
|
410
|
+
if len(value_shape) == 0:
|
411
|
+
modify_args += ["source-value", value]
|
412
|
+
else:
|
413
|
+
prev_indices = []
|
414
|
+
recur_dimensions(prev_indices, value_arr, modify_args)
|
415
|
+
|
416
|
+
if unit is not None:
|
417
|
+
modify_args += ["source-unit", unit]
|
418
|
+
|
419
|
+
if uncertainty_info is not None:
|
420
|
+
for uncertainty_key in uncertainty_info:
|
421
|
+
if uncertainty_key not in STANDARD_KEYS_SCLAR_OR_WITH_EXTENT:
|
422
|
+
raise KIMTestDriverError(
|
423
|
+
"Uncertainty key %s is not one of the allowed options %s."
|
424
|
+
% (uncertainty_key, str(STANDARD_KEYS_SCLAR_OR_WITH_EXTENT))
|
425
|
+
)
|
426
|
+
uncertainty_value = uncertainty_info[uncertainty_key]
|
427
|
+
uncertainty_value_arr = np.array(uncertainty_value)
|
428
|
+
uncertainty_value_shape = uncertainty_value_arr.shape
|
429
|
+
|
430
|
+
if not (
|
431
|
+
len(uncertainty_value_shape) == 0
|
432
|
+
or uncertainty_value_shape == value_shape
|
433
|
+
):
|
434
|
+
raise KIMTestDriverError(
|
435
|
+
f"The value {uncertainty_value_arr} provided for uncertainty key "
|
436
|
+
f"{uncertainty_key} has shape {uncertainty_value_shape}.\n"
|
437
|
+
f"It must either be a scalar or match the shape {value_shape} of "
|
438
|
+
"the source value you provided."
|
439
|
+
)
|
440
|
+
if len(uncertainty_value_shape) == 0:
|
441
|
+
modify_args += [uncertainty_key, uncertainty_value]
|
442
|
+
else:
|
443
|
+
prev_indices = []
|
444
|
+
recur_dimensions(
|
445
|
+
prev_indices, uncertainty_value_arr, modify_args, uncertainty_key
|
446
|
+
)
|
447
|
+
|
448
|
+
return kim_property_modify(property_instances, current_instance_index, *modify_args)
|
449
|
+
|
450
|
+
|
451
|
+
class KIMTestDriverError(Exception):
|
452
|
+
def __init__(self, msg) -> None:
|
453
|
+
# Call the base class constructor with the parameters it needs
|
454
|
+
super(KIMTestDriverError, self).__init__(msg)
|
455
|
+
self.msg = msg
|
456
|
+
|
457
|
+
def __str__(self) -> str:
|
458
|
+
return self.msg
|
459
|
+
|
460
|
+
|
461
|
+
class KIMTestDriver(ABC):
|
462
|
+
"""
|
463
|
+
A base class for creating KIM Test Drivers. It has attributes that are likely
|
464
|
+
to be useful to for most KIM tests
|
465
|
+
|
466
|
+
Attributes:
|
467
|
+
__kim_model_name (str, optional):
|
468
|
+
KIM model name, absent if a non-KIM ASE calculator was provided
|
469
|
+
__calc (:obj:`~ase.calculators.calculator.Calculator`):
|
470
|
+
ASE calculator
|
471
|
+
__output_property_instances (str):
|
472
|
+
Property instances, possibly accumulated over multiple invocations of
|
473
|
+
the Test Driver
|
474
|
+
"""
|
475
|
+
|
476
|
+
def __init__(self, model: Union[str, Calculator]) -> None:
|
477
|
+
"""
|
478
|
+
Args:
|
479
|
+
model:
|
480
|
+
ASE calculator or KIM model name to use
|
481
|
+
"""
|
482
|
+
if isinstance(model, Calculator):
|
483
|
+
self.__calc = model
|
484
|
+
self.__kim_model_name = None
|
485
|
+
else:
|
486
|
+
from ase.calculators.kim.kim import KIM
|
487
|
+
|
488
|
+
self.__kim_model_name = model
|
489
|
+
self.__calc = KIM(self.__kim_model_name)
|
490
|
+
|
491
|
+
self.__output_property_instances = "[]"
|
492
|
+
|
493
|
+
def _setup(self, material, **kwargs) -> None:
|
494
|
+
"""
|
495
|
+
Empty method, for optional overrides
|
496
|
+
"""
|
497
|
+
pass
|
498
|
+
|
499
|
+
@abstractmethod
|
500
|
+
def _calculate(self, **kwargs) -> None:
|
501
|
+
"""
|
502
|
+
Abstract calculate method
|
503
|
+
"""
|
504
|
+
raise NotImplementedError("Subclasses must implement the _calculate method.")
|
505
|
+
|
506
|
+
def __call__(self, material: Any = None, **kwargs) -> List[Dict]:
|
507
|
+
"""
|
508
|
+
|
509
|
+
Main operation of a Test Driver:
|
510
|
+
|
511
|
+
* Run :func:`~KIMTestDriver._setup` (the base class provides a barebones
|
512
|
+
version, derived classes may override)
|
513
|
+
* Call :func:`~KIMTestDriver._calculate` (implemented by each individual
|
514
|
+
Test Driver)
|
515
|
+
|
516
|
+
Args:
|
517
|
+
material:
|
518
|
+
Placeholder object for arguments describing the material to run
|
519
|
+
the Test Driver on
|
520
|
+
|
521
|
+
Returns:
|
522
|
+
The property instances calculated during the current run
|
523
|
+
"""
|
524
|
+
# count how many instances we had before we started
|
525
|
+
previous_properties_end = len(kim_edn.loads(self.__output_property_instances))
|
526
|
+
|
527
|
+
# _setup is likely overridden by an derived class
|
528
|
+
self._setup(material, **kwargs)
|
529
|
+
|
530
|
+
# implemented by each individual Test Driver
|
531
|
+
self._calculate(**kwargs)
|
532
|
+
|
533
|
+
# The current invocation returns a Python list of dictionaries containing all
|
534
|
+
# properties computed during this run
|
535
|
+
return kim_edn.loads(self.__output_property_instances)[previous_properties_end:]
|
536
|
+
|
537
|
+
def _add_property_instance(
|
538
|
+
self, property_name: str, disclaimer: Optional[str] = None
|
539
|
+
) -> None:
|
540
|
+
"""
|
541
|
+
Initialize a new property instance.
|
542
|
+
NOTE: Is there any need to allow Test Driver authors to specify an instance-id?
|
543
|
+
|
544
|
+
Args:
|
545
|
+
property_name:
|
546
|
+
The property name, e.g.
|
547
|
+
"tag:staff@noreply.openkim.org,2023-02-21:property/binding-energy-crystal"
|
548
|
+
or "binding-energy-crystal"
|
549
|
+
disclaimer:
|
550
|
+
An optional disclaimer commenting on the applicability of this result,
|
551
|
+
e.g. "This relaxation did not reach the desired tolerance."
|
552
|
+
"""
|
553
|
+
self.__output_property_instances = _add_property_instance(
|
554
|
+
property_name, disclaimer, self.__output_property_instances
|
555
|
+
)
|
556
|
+
|
557
|
+
def _add_key_to_current_property_instance(
|
558
|
+
self,
|
559
|
+
name: str,
|
560
|
+
value: ArrayLike,
|
561
|
+
unit: Optional[str] = None,
|
562
|
+
uncertainty_info: Optional[dict] = None,
|
563
|
+
) -> None:
|
564
|
+
"""
|
565
|
+
Add a key to the most recent property instance added with
|
566
|
+
:func:`~kim_tools.KIMTestDriver._add_property_instance`. If the value is an
|
567
|
+
array, this function will assume you want to write to the beginning of the array
|
568
|
+
in every dimension. This function is intended to write entire keys in one go,
|
569
|
+
and should not be used for modifying existing keys.
|
570
|
+
|
571
|
+
WARNING! It is the developer's responsibility to make sure the array shape
|
572
|
+
matches the extent specified in the property definition. This method uses
|
573
|
+
``kim_property.kim_property_modify``, and fills the values of array keys as
|
574
|
+
slices through the last dimension. If those slices are incomplete, kim_property
|
575
|
+
automatically initializes the other elements in that slice to zero. For example,
|
576
|
+
consider writing coordinates to a key with extent [":", 3]. The correct way to
|
577
|
+
write a single atom would be to provide [[x, y, z]]. If you accidentally provide
|
578
|
+
[[x], [y], [z]], it will fill the field with the coordinates
|
579
|
+
[[x, 0, 0], [y, 0, 0], [z, 0, 0]]. This will not raise an error, only exceeding
|
580
|
+
the allowed dimesnsions of the key will do so.
|
581
|
+
|
582
|
+
Args:
|
583
|
+
name:
|
584
|
+
Name of the key, e.g. "cell-cauchy-stress"
|
585
|
+
value:
|
586
|
+
The value of the key. The function will attempt to convert it to a NumPy
|
587
|
+
array, then use the dimensions of the resulting array. Scalars, lists,
|
588
|
+
tuples, and arrays should work. Data type of the elements should be str,
|
589
|
+
float, int, or bool
|
590
|
+
unit:
|
591
|
+
The units
|
592
|
+
uncertainty_info:
|
593
|
+
dictionary containing any uncertainty keys you wish to include. See
|
594
|
+
https://openkim.org/doc/schema/properties-framework/ for the possible
|
595
|
+
uncertainty key names. These must be the same dimension as ``value``, or
|
596
|
+
they may be scalars regardless of the shape of ``value``.
|
597
|
+
"""
|
598
|
+
self.__output_property_instances = _add_key_to_current_property_instance(
|
599
|
+
self.__output_property_instances, name, value, unit, uncertainty_info
|
600
|
+
)
|
601
|
+
|
602
|
+
def _add_file_to_current_property_instance(
|
603
|
+
self, name: str, filename: os.PathLike, add_instance_id: bool = True
|
604
|
+
) -> None:
|
605
|
+
"""
|
606
|
+
add a "file" type key-value pair to the current property instance.
|
607
|
+
|
608
|
+
Args:
|
609
|
+
name:
|
610
|
+
Name of the key, e.g. "restart-file"
|
611
|
+
filename:
|
612
|
+
The path to the filename. If it is not in "$CWD/output/",
|
613
|
+
the file will be moved there
|
614
|
+
add_instance_id:
|
615
|
+
By default, a numerical index will be added before the file extension or
|
616
|
+
at the end of a file with no extension. This is to ensure files do not
|
617
|
+
get overwritten when the _calculate method is called repeatedly.
|
618
|
+
|
619
|
+
Raises:
|
620
|
+
KIMTestDriverError:
|
621
|
+
If the provided filename does not exist
|
622
|
+
"""
|
623
|
+
if not os.path.isfile(filename):
|
624
|
+
raise KIMTestDriverError("Provided file {filename} does not exist.")
|
625
|
+
|
626
|
+
# all paths here should be absolute
|
627
|
+
cwd_path = Path(os.getcwd())
|
628
|
+
output_path = cwd_path / "output"
|
629
|
+
filename_path = Path(filename).resolve()
|
630
|
+
|
631
|
+
if output_path not in filename_path.parents:
|
632
|
+
# Need to move file to output
|
633
|
+
if cwd_path in filename_path.parents:
|
634
|
+
# The file is somewhere under CWD,
|
635
|
+
# so move it under CWD/output with
|
636
|
+
# its whole directory structure
|
637
|
+
final_dir = os.path.join(
|
638
|
+
output_path, os.path.relpath(filename_path.parent)
|
639
|
+
)
|
640
|
+
os.makedirs(final_dir, exist_ok=True)
|
641
|
+
else:
|
642
|
+
# We got a file that isn't even under CWD.
|
643
|
+
# I can't really hope to suss out what they
|
644
|
+
# were going for, so just move it to CWD/output
|
645
|
+
final_dir = output_path
|
646
|
+
else:
|
647
|
+
# already under output, not moving anything
|
648
|
+
final_dir = filename_path.parent
|
649
|
+
|
650
|
+
input_name = filename_path.name
|
651
|
+
if add_instance_id:
|
652
|
+
current_instance_id = len(kim_edn.loads(self.__output_property_instances))
|
653
|
+
root, ext = os.path.splitext(input_name)
|
654
|
+
root = root + "-" + str(current_instance_id)
|
655
|
+
final_name = root + ext
|
656
|
+
else:
|
657
|
+
final_name = input_name
|
658
|
+
|
659
|
+
final_path = os.path.join(final_dir, final_name)
|
660
|
+
|
661
|
+
assert final_path.startswith(str(output_path))
|
662
|
+
|
663
|
+
shutil.move(filename, final_path)
|
664
|
+
|
665
|
+
# Filenames are reported relative to $CWD/output
|
666
|
+
self._add_key_to_current_property_instance(
|
667
|
+
name, os.path.relpath(final_path, output_path)
|
668
|
+
)
|
669
|
+
|
670
|
+
@property
|
671
|
+
def kim_model_name(self) -> Optional[str]:
|
672
|
+
"""
|
673
|
+
Get the KIM model name, if present
|
674
|
+
"""
|
675
|
+
return self.__kim_model_name
|
676
|
+
|
677
|
+
@property
|
678
|
+
def property_instances(self) -> Dict:
|
679
|
+
"""
|
680
|
+
Get all property instances accumulated over all calls to the Test Driver so far
|
681
|
+
"""
|
682
|
+
return kim_edn.loads(self.__output_property_instances)
|
683
|
+
|
684
|
+
@property
|
685
|
+
def _calc(self) -> Optional[Calculator]:
|
686
|
+
"""
|
687
|
+
Get the ASE calculator
|
688
|
+
"""
|
689
|
+
return self.__calc
|
690
|
+
|
691
|
+
def _get_serialized_property_instances(self) -> str:
|
692
|
+
"""
|
693
|
+
Get the property instances computed so far in serialized EDN format
|
694
|
+
"""
|
695
|
+
return self.__output_property_instances
|
696
|
+
|
697
|
+
def _set_serialized_property_instances(self, property_instances: str) -> None:
|
698
|
+
"""
|
699
|
+
Set the property instances from a serialized EDN string
|
700
|
+
"""
|
701
|
+
self.__output_property_instances = property_instances
|
702
|
+
|
703
|
+
def write_property_instances_to_file(self, filename="output/results.edn") -> None:
|
704
|
+
"""
|
705
|
+
Write internal property instances (possibly accumulated over several calls to
|
706
|
+
the Test Driver) to a file at the requested path. Also dumps any cached files to
|
707
|
+
the same directory.
|
708
|
+
|
709
|
+
Args:
|
710
|
+
filename: path to write the file
|
711
|
+
"""
|
712
|
+
|
713
|
+
with open(filename, "w") as f:
|
714
|
+
kim_property_dump(
|
715
|
+
self.__output_property_instances, f
|
716
|
+
) # serialize the dictionary to string first
|
717
|
+
|
718
|
+
|
719
|
+
def _add_common_crystal_genome_keys_to_current_property_instance(
|
720
|
+
property_instances: str,
|
721
|
+
prototype_label: str,
|
722
|
+
stoichiometric_species: List[str],
|
723
|
+
a: float,
|
724
|
+
parameter_values: Optional[List[float]] = None,
|
725
|
+
library_prototype_label: Optional[Union[List[str], str]] = None,
|
726
|
+
short_name: Optional[Union[List[str], str]] = None,
|
727
|
+
cell_cauchy_stress: Optional[List[float]] = None,
|
728
|
+
temperature: Optional[float] = None,
|
729
|
+
crystal_genome_source_structure_id: Optional[List[List[str]]] = None,
|
730
|
+
a_unit: str = "angstrom",
|
731
|
+
cell_cauchy_stress_unit: str = "eV/angstrom^3",
|
732
|
+
temperature_unit: str = "K",
|
733
|
+
) -> str:
|
734
|
+
"""
|
735
|
+
Write common Crystal Genome keys to the last element of ``property_instances``. See
|
736
|
+
https://openkim.org/properties/show/crystal-structure-npt for definition of the
|
737
|
+
input keys. Note that the "parameter-names" key is inferred from the
|
738
|
+
``prototype_label`` input and is not an input to this function.
|
739
|
+
|
740
|
+
Args:
|
741
|
+
property_instances:
|
742
|
+
An EDN-serialized list of dictionaries representing KIM Property Instances.
|
743
|
+
The key will be added to the last dictionary in the list
|
744
|
+
|
745
|
+
Returns:
|
746
|
+
Updated EDN-serialized list of property instances
|
747
|
+
"""
|
748
|
+
property_instances = _add_key_to_current_property_instance(
|
749
|
+
property_instances, "prototype-label", prototype_label
|
750
|
+
)
|
751
|
+
property_instances = _add_key_to_current_property_instance(
|
752
|
+
property_instances, "stoichiometric-species", stoichiometric_species
|
753
|
+
)
|
754
|
+
property_instances = _add_key_to_current_property_instance(
|
755
|
+
property_instances, "a", a, a_unit
|
756
|
+
)
|
757
|
+
|
758
|
+
# get parameter names
|
759
|
+
aflow = AFLOW()
|
760
|
+
aflow_parameter_names = aflow.get_param_names_from_prototype(prototype_label)
|
761
|
+
if parameter_values is None:
|
762
|
+
if len(aflow_parameter_names) > 1:
|
763
|
+
raise KIMTestDriverError(
|
764
|
+
"The prototype label implies that parameter_values (i.e. dimensionless "
|
765
|
+
"parameters besides a) are required, but you provided None"
|
766
|
+
)
|
767
|
+
else:
|
768
|
+
if len(aflow_parameter_names) - 1 != len(parameter_values):
|
769
|
+
raise KIMTestDriverError(
|
770
|
+
"Incorrect number of parameter_values (i.e. dimensionless parameters "
|
771
|
+
"besides a) for the provided prototype"
|
772
|
+
)
|
773
|
+
property_instances = _add_key_to_current_property_instance(
|
774
|
+
property_instances, "parameter-names", aflow_parameter_names[1:]
|
775
|
+
)
|
776
|
+
property_instances = _add_key_to_current_property_instance(
|
777
|
+
property_instances, "parameter-values", parameter_values
|
778
|
+
)
|
779
|
+
|
780
|
+
if short_name is not None:
|
781
|
+
if not isinstance(short_name, list):
|
782
|
+
short_name = [short_name]
|
783
|
+
property_instances = _add_key_to_current_property_instance(
|
784
|
+
property_instances, "short-name", short_name
|
785
|
+
)
|
786
|
+
|
787
|
+
if library_prototype_label is not None:
|
788
|
+
property_instances = _add_key_to_current_property_instance(
|
789
|
+
property_instances, "library-prototype-label", library_prototype_label
|
790
|
+
)
|
791
|
+
|
792
|
+
if cell_cauchy_stress is not None:
|
793
|
+
if len(cell_cauchy_stress) != 6:
|
794
|
+
raise KIMTestDriverError(
|
795
|
+
"Please specify the Cauchy stress as a 6-dimensional vector in Voigt "
|
796
|
+
"order [xx, yy, zz, yz, xz, xy]"
|
797
|
+
)
|
798
|
+
property_instances = _add_key_to_current_property_instance(
|
799
|
+
property_instances,
|
800
|
+
"cell-cauchy-stress",
|
801
|
+
cell_cauchy_stress,
|
802
|
+
cell_cauchy_stress_unit,
|
803
|
+
)
|
804
|
+
|
805
|
+
if temperature is not None:
|
806
|
+
property_instances = _add_key_to_current_property_instance(
|
807
|
+
property_instances, "temperature", temperature, temperature_unit
|
808
|
+
)
|
809
|
+
|
810
|
+
if crystal_genome_source_structure_id is not None:
|
811
|
+
property_instances = _add_key_to_current_property_instance(
|
812
|
+
property_instances,
|
813
|
+
"crystal-genome-source-structure-id",
|
814
|
+
crystal_genome_source_structure_id,
|
815
|
+
)
|
816
|
+
|
817
|
+
return property_instances
|
818
|
+
|
819
|
+
|
820
|
+
def _add_property_instance_and_common_crystal_genome_keys(
|
821
|
+
property_name: str,
|
822
|
+
prototype_label: str,
|
823
|
+
stoichiometric_species: List[str],
|
824
|
+
a: float,
|
825
|
+
parameter_values: Optional[List[float]] = None,
|
826
|
+
library_prototype_label: Optional[Union[List[str], str]] = None,
|
827
|
+
short_name: Optional[Union[List[str], str]] = None,
|
828
|
+
cell_cauchy_stress: Optional[List[float]] = None,
|
829
|
+
temperature: Optional[float] = None,
|
830
|
+
crystal_genome_source_structure_id: Optional[List[List[str]]] = None,
|
831
|
+
a_unit: str = "angstrom",
|
832
|
+
cell_cauchy_stress_unit: str = "eV/angstrom^3",
|
833
|
+
temperature_unit: str = "K",
|
834
|
+
disclaimer: Optional[str] = None,
|
835
|
+
property_instances: Optional[str] = None,
|
836
|
+
) -> str:
|
837
|
+
"""
|
838
|
+
Initialize a new property instance to ``property_instances`` (an empty
|
839
|
+
``property_instances`` will be initialized if not provided). It will automatically
|
840
|
+
get the an "instance-id" equal to the length of ``property_instances`` after it is
|
841
|
+
added. Then, write the common Crystal Genome keys to it. See
|
842
|
+
https://openkim.org/properties/show/crystal-structure-npt for definition of the
|
843
|
+
input keys. Note that the "parameter-names" key is inferred from the
|
844
|
+
``prototype_label`` input and is not an input to this function.
|
845
|
+
|
846
|
+
Args:
|
847
|
+
property_name:
|
848
|
+
The property name, e.g.
|
849
|
+
"tag:staff@noreply.openkim.org,2023-02-21:property/binding-energy-crystal"
|
850
|
+
or "binding-energy-crystal"
|
851
|
+
disclaimer:
|
852
|
+
An optional disclaimer commenting on the applicability of this result, e.g.
|
853
|
+
"This relaxation did not reach the desired tolerance."
|
854
|
+
property_instances:
|
855
|
+
A pre-existing EDN-serialized list of KIM Property instances to add to
|
856
|
+
|
857
|
+
Returns:
|
858
|
+
Updated EDN-serialized list of property instances
|
859
|
+
"""
|
860
|
+
property_instances = _add_property_instance(
|
861
|
+
property_name, disclaimer, property_instances
|
862
|
+
)
|
863
|
+
return _add_common_crystal_genome_keys_to_current_property_instance(
|
864
|
+
property_instances,
|
865
|
+
prototype_label,
|
866
|
+
stoichiometric_species,
|
867
|
+
a,
|
868
|
+
parameter_values,
|
869
|
+
library_prototype_label,
|
870
|
+
short_name,
|
871
|
+
cell_cauchy_stress,
|
872
|
+
temperature,
|
873
|
+
crystal_genome_source_structure_id,
|
874
|
+
a_unit,
|
875
|
+
cell_cauchy_stress_unit,
|
876
|
+
temperature_unit,
|
877
|
+
)
|
878
|
+
|
879
|
+
|
880
|
+
def get_crystal_structure_from_atoms(
|
881
|
+
atoms: Atoms, get_short_name: bool = True, prim: bool = True, aflow_np: int = 4
|
882
|
+
) -> Dict:
|
883
|
+
"""
|
884
|
+
By performing a symmetry analysis on an :class:`~ase.Atoms` object, generate a
|
885
|
+
dictionary that is a subset of the
|
886
|
+
`crystal-structure-npt <https://openkim.org/properties/show/crystal-structure-npt>`_
|
887
|
+
property. See https://openkim.org/doc/schema/properties-framework/ for more
|
888
|
+
information about KIM properties and how values and units are defined. The
|
889
|
+
dictionary returned by this function does not necessarily constitute a complete KIM
|
890
|
+
Property Instance, but the key-value pairs are in valid KIM Property format and can
|
891
|
+
be inserted as-is into any single crystal Crystal Genome property.
|
892
|
+
|
893
|
+
Args:
|
894
|
+
atoms: Configuration to analyze. It is assumed that the length unit is angstrom
|
895
|
+
get_short_name:
|
896
|
+
whether to compare against AFLOW prototype library to obtain short-name
|
897
|
+
prim: whether to primitivize the atoms object first
|
898
|
+
aflow_np: Number of processors to use with AFLOW executable
|
899
|
+
|
900
|
+
Returns:
|
901
|
+
A dictionary that has the following Property Keys (possibly optionally) defined.
|
902
|
+
See the
|
903
|
+
`crystal-structure-npt
|
904
|
+
<https://openkim.org/properties/show/crystal-structure-npt>`_
|
905
|
+
Property Definition for their meaning:
|
906
|
+
|
907
|
+
- "stoichiometric-species"
|
908
|
+
- "prototype-label"
|
909
|
+
- "a"
|
910
|
+
- "parameter-names" (inferred from "prototype-label")
|
911
|
+
- "parameter-values"
|
912
|
+
- "short-name"
|
913
|
+
|
914
|
+
"""
|
915
|
+
aflow = AFLOW(np=aflow_np)
|
916
|
+
|
917
|
+
proto_des = aflow.get_prototype_designation_from_atoms(atoms, prim=prim)
|
918
|
+
library_prototype_label, short_name = (
|
919
|
+
aflow.get_library_prototype_label_and_shortname_from_atoms(atoms, prim=prim)
|
920
|
+
if get_short_name
|
921
|
+
else (None, None)
|
922
|
+
)
|
923
|
+
|
924
|
+
a = proto_des["aflow_prototype_params_values"][0]
|
925
|
+
parameter_values = (
|
926
|
+
proto_des["aflow_prototype_params_values"][1:]
|
927
|
+
if len(proto_des["aflow_prototype_params_values"]) > 1
|
928
|
+
else None
|
929
|
+
)
|
930
|
+
|
931
|
+
property_instances = _add_property_instance_and_common_crystal_genome_keys(
|
932
|
+
property_name="crystal-structure-npt",
|
933
|
+
prototype_label=proto_des["aflow_prototype_label"],
|
934
|
+
stoichiometric_species=sorted(list(set(atoms.get_chemical_symbols()))),
|
935
|
+
a=a,
|
936
|
+
parameter_values=parameter_values,
|
937
|
+
library_prototype_label=library_prototype_label,
|
938
|
+
short_name=short_name,
|
939
|
+
)
|
940
|
+
|
941
|
+
return kim_edn.loads(property_instances)[0]
|
942
|
+
|
943
|
+
|
944
|
+
def get_poscar_from_crystal_structure(
|
945
|
+
crystal_structure: Dict, output_file: Optional[str] = None, flat: bool = False
|
946
|
+
) -> Optional[str]:
|
947
|
+
"""
|
948
|
+
Write a POSCAR coordinate file (or output it as a multiline string) from the AFLOW
|
949
|
+
Prototype Designation obtained from a KIM Property Instance. The following keys from
|
950
|
+
https://openkim.org/properties/show/2023-02-21/staff@noreply.openkim.org/crystal-structure-npt
|
951
|
+
are used:
|
952
|
+
|
953
|
+
- "stoichiometric-species"
|
954
|
+
- "prototype-label"
|
955
|
+
- "a"
|
956
|
+
- "parameter-values"
|
957
|
+
(if "prototype-label" defines a crystal with free parameters besides "a")
|
958
|
+
|
959
|
+
Note that although the keys are required to follow the schema of a KIM Property
|
960
|
+
Instance (https://openkim.org/doc/schema/properties-framework/), there is no
|
961
|
+
requirement or check that the input dictionary is an instance of any specific KIM
|
962
|
+
Property, only that the keys required to build a crystal are present.
|
963
|
+
|
964
|
+
Args:
|
965
|
+
crystal_structure:
|
966
|
+
Dictionary containing the required keys in KIM Property Instance format
|
967
|
+
output_file:
|
968
|
+
Name of the output file. If not provided, the output is returned as a string
|
969
|
+
flat:
|
970
|
+
whether the input dictionary is flattened
|
971
|
+
Returns:
|
972
|
+
If ``output_file`` is not provided, a string in POSCAR format containg the
|
973
|
+
primitive unit cell of the crystal as defined in
|
974
|
+
http://doi.org/10.1016/j.commatsci.2017.01.017. Lengths are always in angstrom.
|
975
|
+
Raises:
|
976
|
+
AFLOW.ChangedSymmetryException: if the symmetry of the atoms object is different
|
977
|
+
from ``prototype_label``
|
978
|
+
"""
|
979
|
+
if flat:
|
980
|
+
prototype_label = crystal_structure["prototype-label.source-value"]
|
981
|
+
a_unit = crystal_structure["a.source-unit"]
|
982
|
+
a_value = crystal_structure["a.source-value"]
|
983
|
+
parameter_values_key = "parameter-values.source-value"
|
984
|
+
stoichiometric_species = crystal_structure[
|
985
|
+
"stoichiometric-species.source-value"
|
986
|
+
]
|
987
|
+
else:
|
988
|
+
prototype_label = crystal_structure["prototype-label"]["source-value"]
|
989
|
+
a_unit = crystal_structure["a"]["source-unit"]
|
990
|
+
a_value = crystal_structure["a"]["source-value"]
|
991
|
+
parameter_values_key = "parameter-values"
|
992
|
+
stoichiometric_species = crystal_structure["stoichiometric-species"][
|
993
|
+
"source-value"
|
994
|
+
]
|
995
|
+
|
996
|
+
aflow = AFLOW()
|
997
|
+
aflow_parameter_names = aflow.get_param_names_from_prototype(prototype_label)
|
998
|
+
|
999
|
+
# Atoms objects are always in angstrom
|
1000
|
+
if a_unit == "angstrom":
|
1001
|
+
a_angstrom = a_value
|
1002
|
+
else:
|
1003
|
+
a_angstrom = convert_units(
|
1004
|
+
a_value,
|
1005
|
+
crystal_structure["a"]["source-unit"],
|
1006
|
+
a_unit,
|
1007
|
+
True,
|
1008
|
+
)
|
1009
|
+
|
1010
|
+
aflow_parameter_values = [a_angstrom]
|
1011
|
+
|
1012
|
+
if parameter_values_key in crystal_structure:
|
1013
|
+
if len(aflow_parameter_names) == 1:
|
1014
|
+
raise KIMTestDriverError(
|
1015
|
+
f'Prototype label {prototype_label} implies only "a" parameter, but '
|
1016
|
+
'you provided "parameter-values"'
|
1017
|
+
)
|
1018
|
+
if flat:
|
1019
|
+
aflow_parameter_values += crystal_structure[parameter_values_key]
|
1020
|
+
else:
|
1021
|
+
aflow_parameter_values += crystal_structure[parameter_values_key][
|
1022
|
+
"source-value"
|
1023
|
+
]
|
1024
|
+
|
1025
|
+
try:
|
1026
|
+
return aflow.write_poscar_from_prototype(
|
1027
|
+
prototype_label=prototype_label,
|
1028
|
+
species=stoichiometric_species,
|
1029
|
+
parameter_values=aflow_parameter_values,
|
1030
|
+
output_file=output_file,
|
1031
|
+
)
|
1032
|
+
except AFLOW.ChangedSymmetryException as e:
|
1033
|
+
# re-raise, just indicating that this function knows about this exception
|
1034
|
+
raise e
|
1035
|
+
|
1036
|
+
|
1037
|
+
def get_atoms_from_crystal_structure(
|
1038
|
+
crystal_structure: Dict, flat: bool = False
|
1039
|
+
) -> Atoms:
|
1040
|
+
"""
|
1041
|
+
Generate an :class:`~ase.Atoms` object from the AFLOW Prototype Designation obtained
|
1042
|
+
from a KIM Property Instance. The following keys from
|
1043
|
+
https://openkim.org/properties/show/2023-02-21/staff@noreply.openkim.org/crystal-structure-npt
|
1044
|
+
are used:
|
1045
|
+
|
1046
|
+
- "stoichiometric-species"
|
1047
|
+
- "prototype-label"
|
1048
|
+
- "a"
|
1049
|
+
- "parameter-values" (if "prototype-label" defines a crystal with free parameters
|
1050
|
+
besides "a")
|
1051
|
+
|
1052
|
+
Note that although the keys are required to follow the schema of a KIM Property
|
1053
|
+
Instance (https://openkim.org/doc/schema/properties-framework/),
|
1054
|
+
There is no requirement or check that the input dictionary is an instance of any
|
1055
|
+
specific KIM Property, only that the keys required to build a crystal are present.
|
1056
|
+
|
1057
|
+
Args:
|
1058
|
+
crystal_structure:
|
1059
|
+
Dictionary containing the required keys in KIM Property Instance format
|
1060
|
+
flat:
|
1061
|
+
whether the dictionary is flattened
|
1062
|
+
|
1063
|
+
Returns:
|
1064
|
+
Primitive unit cell of the crystal as defined in the
|
1065
|
+
`AFLOW prototype standard <http://doi.org/10.1016/j.commatsci.2017.01.017>`_.
|
1066
|
+
Lengths are always in angstrom
|
1067
|
+
|
1068
|
+
Raises:
|
1069
|
+
AFLOW.ChangedSymmetryException:
|
1070
|
+
if the symmetry of the atoms object is different from ``prototype_label``
|
1071
|
+
"""
|
1072
|
+
try:
|
1073
|
+
poscar_string = get_poscar_from_crystal_structure(crystal_structure, flat=flat)
|
1074
|
+
except AFLOW.ChangedSymmetryException as e:
|
1075
|
+
# re-raise, just indicating that this function knows about this exception
|
1076
|
+
raise e
|
1077
|
+
|
1078
|
+
with NamedTemporaryFile(mode="w+") as f:
|
1079
|
+
f.write(poscar_string)
|
1080
|
+
f.seek(0)
|
1081
|
+
atoms = ase.io.read(f.name, format="vasp")
|
1082
|
+
atoms.wrap()
|
1083
|
+
return atoms
|
1084
|
+
|
1085
|
+
|
1086
|
+
class SingleCrystalTestDriver(KIMTestDriver):
|
1087
|
+
"""
|
1088
|
+
A KIM test that computes property(s) of a single nominal crystal structure
|
1089
|
+
|
1090
|
+
Attributes:
|
1091
|
+
__nominal_crystal_structure_npt [Dict]:
|
1092
|
+
An instance of the
|
1093
|
+
`crystal-structure-npt
|
1094
|
+
<https://openkim.org/properties/show/crystal-structure-npt>`_
|
1095
|
+
property representing the nominal crystal structure and conditions of the
|
1096
|
+
current call to the Test Driver.
|
1097
|
+
"""
|
1098
|
+
|
1099
|
+
def _setup(
|
1100
|
+
self,
|
1101
|
+
material: Union[Atoms, Dict],
|
1102
|
+
cell_cauchy_stress_eV_angstrom3: Optional[List[float]] = None,
|
1103
|
+
temperature_K: float = 0,
|
1104
|
+
**kwargs,
|
1105
|
+
) -> None:
|
1106
|
+
"""
|
1107
|
+
TODO: Consider allowing arbitrary units for temp and stress?
|
1108
|
+
|
1109
|
+
Args:
|
1110
|
+
material:
|
1111
|
+
An :class:`~ase.Atoms` object or a KIM Property Instance specifying the
|
1112
|
+
nominal crystal structure that this run of the test
|
1113
|
+
will use. Pass one of the two following types of objects:
|
1114
|
+
|
1115
|
+
Atoms object:
|
1116
|
+
|
1117
|
+
:class:`~ase.Atoms` object to use as the initial configuration. Note
|
1118
|
+
that a symmetry analysis will be performed on it and a primitive
|
1119
|
+
cell will be generated according to the conventions in
|
1120
|
+
http://doi.org/10.1016/j.commatsci.2017.01.017. This primitive cell
|
1121
|
+
may be rotated and translated relative to the configuration you
|
1122
|
+
provided.
|
1123
|
+
|
1124
|
+
Property instance:
|
1125
|
+
|
1126
|
+
Dictionary containing information about the nominal input crystal
|
1127
|
+
structure in KIM Property Instance format (e.g. from a query to the
|
1128
|
+
OpenKIM.org database)
|
1129
|
+
The following keys from
|
1130
|
+
https://openkim.org/properties/show/2023-02-21/staff@noreply.openkim.org/crystal-structure-npt
|
1131
|
+
are used:
|
1132
|
+
|
1133
|
+
- "stoichiometric-species"
|
1134
|
+
- "prototype-label"
|
1135
|
+
- "a"
|
1136
|
+
- "parameter-values"
|
1137
|
+
(if "prototype-label" defines a crystal with
|
1138
|
+
free parameters besides "a")
|
1139
|
+
- "short-name" (if present)
|
1140
|
+
- "crystal-genome-source-structure-id" (if present)
|
1141
|
+
|
1142
|
+
Note that although the keys are required to follow the schema of a
|
1143
|
+
KIM Property Instance
|
1144
|
+
(https://openkim.org/doc/schema/properties-framework/),
|
1145
|
+
There is no requirement or check that the input dictionary is an
|
1146
|
+
instance of any specific KIM Property, only that
|
1147
|
+
the keys required to build a crystal are present.
|
1148
|
+
|
1149
|
+
cell_cauchy_stress_eV_angstrom3:
|
1150
|
+
Cauchy stress on the cell in eV/angstrom^3 (ASE units) in
|
1151
|
+
[xx, yy, zz, yz, xz, xy] format. This is a nominal variable, and this
|
1152
|
+
class simply provides recordkeeping of it. It is up to derived classes
|
1153
|
+
to implement actually imposing this stress on the system.
|
1154
|
+
temperature_K:
|
1155
|
+
The temperature in Kelvin. This is a nominal variable, and this class
|
1156
|
+
simply provides recordkeeping of it. It is up to derived classes to
|
1157
|
+
implement actually setting the temperature of the system.
|
1158
|
+
"""
|
1159
|
+
if cell_cauchy_stress_eV_angstrom3 is None:
|
1160
|
+
cell_cauchy_stress_eV_angstrom3 = [0, 0, 0, 0, 0, 0]
|
1161
|
+
|
1162
|
+
if isinstance(material, Atoms):
|
1163
|
+
crystal_structure = get_crystal_structure_from_atoms(material)
|
1164
|
+
msg = (
|
1165
|
+
"Rebuilding atoms object in a standard setting defined by "
|
1166
|
+
"doi.org/10.1016/j.commatsci.2017.01.017. See log file or computed "
|
1167
|
+
"properties for the (possibly re-oriented) primitive cell that "
|
1168
|
+
"computations will be based on."
|
1169
|
+
)
|
1170
|
+
logger.info(msg)
|
1171
|
+
print()
|
1172
|
+
print(msg)
|
1173
|
+
print()
|
1174
|
+
else:
|
1175
|
+
crystal_structure = material
|
1176
|
+
|
1177
|
+
# Pop the temperature and stress keys in case they came along with a query
|
1178
|
+
if "temperature" in crystal_structure:
|
1179
|
+
crystal_structure.pop("temperature")
|
1180
|
+
if "cell-cauchy-stress" in crystal_structure:
|
1181
|
+
crystal_structure.pop("cell-cauchy-stress")
|
1182
|
+
|
1183
|
+
crystal_structure["temperature"] = {
|
1184
|
+
"source-value": temperature_K,
|
1185
|
+
"source-unit": "K",
|
1186
|
+
}
|
1187
|
+
crystal_structure["cell-cauchy-stress"] = {
|
1188
|
+
"source-value": cell_cauchy_stress_eV_angstrom3,
|
1189
|
+
"source-unit": "eV/angstrom^3",
|
1190
|
+
}
|
1191
|
+
if "meta" in crystal_structure:
|
1192
|
+
# Carrying 'meta' around doesn't really make sense since we may have already
|
1193
|
+
# modified things and will modify in the future
|
1194
|
+
crystal_structure.pop("meta")
|
1195
|
+
|
1196
|
+
self.__nominal_crystal_structure_npt = crystal_structure
|
1197
|
+
|
1198
|
+
# Warn if atoms appear unrelaxed
|
1199
|
+
atoms_tmp = self._get_atoms()
|
1200
|
+
force_max = np.max(atoms_tmp.get_forces())
|
1201
|
+
if force_max > FMAX_INITIAL:
|
1202
|
+
msg = (
|
1203
|
+
"The configuration you provided has a maximum force component "
|
1204
|
+
f"{force_max} eV/angstrom. Unless the Test Driver you are running "
|
1205
|
+
"provides minimization, you may wish to relax the configuration."
|
1206
|
+
)
|
1207
|
+
print(f"\nNOTE: {msg}\n")
|
1208
|
+
logger.info(msg)
|
1209
|
+
if cell_cauchy_stress_eV_angstrom3 != [0, 0, 0, 0, 0, 0]:
|
1210
|
+
stress_max = np.max(atoms_tmp.get_stress())
|
1211
|
+
if stress_max > FMAX_INITIAL:
|
1212
|
+
msg = (
|
1213
|
+
"The configuration you provided has a maximum stress component "
|
1214
|
+
f"{stress_max} eV/angstrom^3 even though the nominal state of the "
|
1215
|
+
"system is unstressed. Unless the Test Driver you are running "
|
1216
|
+
"provides minimization, you may wish to relax the configuration."
|
1217
|
+
)
|
1218
|
+
print(f"\nNOTE: {msg}\n")
|
1219
|
+
logger.info(msg)
|
1220
|
+
|
1221
|
+
def _update_nominal_parameter_values(self, atoms: Atoms) -> None:
|
1222
|
+
"""
|
1223
|
+
Update the nominal parameter values of the nominal crystal structure from the
|
1224
|
+
provided :class:`~ase.Atoms` object. It is assumed that the crystallographic
|
1225
|
+
symmetry (space group + occupied Wyckoff positions) have not changed from the
|
1226
|
+
initially provided structure.
|
1227
|
+
|
1228
|
+
The provided :class:`~ase.Atoms` object MUST be a primitive cell of the crystal
|
1229
|
+
as defined in http://doi.org/10.1016/j.commatsci.2017.01.017.
|
1230
|
+
The :class:`~ase.Atoms` object may be rotated, translated, and permuted, but the
|
1231
|
+
identity of the lattice vectors must be unchanged w.r.t. the crystallographic
|
1232
|
+
prototype. In other words, there must exist a permutation and translation of the
|
1233
|
+
fractional coordinates that enables them to match the equations defined by the
|
1234
|
+
prototype label.
|
1235
|
+
|
1236
|
+
In practical terms, this means that this function is designed to take as input a
|
1237
|
+
relaxed or time-averaged from MD (and folded back into the original primitive
|
1238
|
+
cell) copy of the :class:`~ase.Atoms` object originally obtained from
|
1239
|
+
:func:`~kim_tools.SingleCrystalTestDriver._get_atoms()`.
|
1240
|
+
|
1241
|
+
If finding the parameter fails, this function will raise an exception. This
|
1242
|
+
probably indicates a phase transition to a different symmetry, which is a normal
|
1243
|
+
occasional occurrence if the original structure is not stable under the
|
1244
|
+
interatomic potential and prescribed conditions. These exceptions should not be
|
1245
|
+
handled and that run of the Test Driver should be allowed to fail.
|
1246
|
+
|
1247
|
+
Args:
|
1248
|
+
atoms: Structure to analyze to get the new parameter values
|
1249
|
+
|
1250
|
+
Raises:
|
1251
|
+
AFLOW.FailedToMatchException:
|
1252
|
+
If the solution failed due to a failure to match two crystals that
|
1253
|
+
should be identical at some point in the solution process. This
|
1254
|
+
*usually* indicates a phase transformation
|
1255
|
+
AFLOW.ChangedSymmetryException:
|
1256
|
+
If a more definitive error indicating a phase transformation is
|
1257
|
+
encountered
|
1258
|
+
"""
|
1259
|
+
|
1260
|
+
try:
|
1261
|
+
aflow_parameter_values = AFLOW().solve_for_params_of_known_prototype(
|
1262
|
+
atoms,
|
1263
|
+
self.__nominal_crystal_structure_npt["prototype-label"]["source-value"],
|
1264
|
+
)
|
1265
|
+
except (AFLOW.FailedToMatchException, AFLOW.ChangedSymmetryException) as e:
|
1266
|
+
raise type(e)(
|
1267
|
+
"Encountered an error that MAY be the result of the nominal crystal "
|
1268
|
+
"being unstable under the given potential and conditions. Stopping "
|
1269
|
+
"execution."
|
1270
|
+
) from e
|
1271
|
+
|
1272
|
+
# Atoms objects always in angstrom
|
1273
|
+
self.__nominal_crystal_structure_npt["a"] = {
|
1274
|
+
"source-value": aflow_parameter_values[0],
|
1275
|
+
"source-unit": "angstrom",
|
1276
|
+
}
|
1277
|
+
if len(aflow_parameter_values) > 1:
|
1278
|
+
self.__nominal_crystal_structure_npt["parameter-values"] = {
|
1279
|
+
"source-value": aflow_parameter_values[1:]
|
1280
|
+
}
|
1281
|
+
|
1282
|
+
def _verify_unchanged_symmetry(self, atoms: Atoms) -> bool:
|
1283
|
+
"""
|
1284
|
+
Without changing the nominal state of the system, check if the provided Atoms
|
1285
|
+
object has the same symmetry as the nominal crystal structure associated with
|
1286
|
+
the current state of the Test Driver. This is defined as having the same
|
1287
|
+
prototype label, except for possible changes in Wyckoff letters as permitted by
|
1288
|
+
the space group normalizer.
|
1289
|
+
|
1290
|
+
Args:
|
1291
|
+
atoms: The structure to compare to the current nominal structure of the Test
|
1292
|
+
Driver
|
1293
|
+
|
1294
|
+
Returns:
|
1295
|
+
Whether or not the symmetry is unchanged
|
1296
|
+
|
1297
|
+
"""
|
1298
|
+
return prototype_labels_are_equivalent(
|
1299
|
+
AFLOW().get_prototype_designation_from_atoms(atoms)[
|
1300
|
+
"aflow_prototype_label"
|
1301
|
+
],
|
1302
|
+
self.__nominal_crystal_structure_npt["prototype-label"]["source-value"],
|
1303
|
+
)
|
1304
|
+
|
1305
|
+
def __add_poscar_to_curr_prop_inst(
|
1306
|
+
self,
|
1307
|
+
change_of_basis: Union[str, ArrayLike],
|
1308
|
+
filename: os.PathLike,
|
1309
|
+
key_name: str,
|
1310
|
+
) -> None:
|
1311
|
+
"""
|
1312
|
+
Add a POSCAR file constructed from ``self.__nominal_crystal_structure_npt``
|
1313
|
+
to the current property instance.
|
1314
|
+
|
1315
|
+
Args:
|
1316
|
+
change_of_basis:
|
1317
|
+
Passed to :meth:`kim_tools.SingleCrystalTestDriver._get_atoms`
|
1318
|
+
filename:
|
1319
|
+
File to save to. Will be automatically moved and renamed,
|
1320
|
+
e.g. 'instance.poscar' -> 'output/instance-1.poscar'
|
1321
|
+
key_name:
|
1322
|
+
The property key to write to
|
1323
|
+
"""
|
1324
|
+
|
1325
|
+
# `_get_atoms` always returns in Angstrom
|
1326
|
+
atoms_tmp = self._get_atoms(change_of_basis)
|
1327
|
+
|
1328
|
+
# will automatically be renamed
|
1329
|
+
# e.g. 'instance.poscar' -> 'output/instance-1.poscar'
|
1330
|
+
atoms_tmp.write(filename=filename, sort=True, format="vasp")
|
1331
|
+
self._add_file_to_current_property_instance(key_name, filename)
|
1332
|
+
|
1333
|
+
def _add_property_instance_and_common_crystal_genome_keys(
|
1334
|
+
self,
|
1335
|
+
property_name: str,
|
1336
|
+
write_stress: bool = False,
|
1337
|
+
write_temp: bool = False,
|
1338
|
+
disclaimer: Optional[str] = None,
|
1339
|
+
) -> None:
|
1340
|
+
"""
|
1341
|
+
Initialize a new property instance to ``self.property_instances``. It will
|
1342
|
+
automatically get the an "instance-id" equal to the length of
|
1343
|
+
``self.property_instances`` after it is added. Then, write the common Crystal
|
1344
|
+
Genome keys to it from the attributes of this class.
|
1345
|
+
|
1346
|
+
Args:
|
1347
|
+
property_name:
|
1348
|
+
The property name, e.g.
|
1349
|
+
"tag:staff@noreply.openkim.org,2023-02-21:property/binding-energy-crystal"
|
1350
|
+
or "binding-energy-crystal"
|
1351
|
+
write_stress:
|
1352
|
+
Write the "cell-cauchy-stress" key
|
1353
|
+
write_temp:
|
1354
|
+
Write the "temperature" key
|
1355
|
+
disclaimer:
|
1356
|
+
An optional disclaimer commenting on the applicability of this result,
|
1357
|
+
e.g. "This relaxation did not reach the desired tolerance."
|
1358
|
+
"""
|
1359
|
+
crystal_structure = self.__nominal_crystal_structure_npt
|
1360
|
+
|
1361
|
+
a = crystal_structure["a"]["source-value"]
|
1362
|
+
|
1363
|
+
a_unit = crystal_structure["a"]["source-unit"]
|
1364
|
+
|
1365
|
+
prototype_label = crystal_structure["prototype-label"]["source-value"]
|
1366
|
+
|
1367
|
+
stoichiometric_species = crystal_structure["stoichiometric-species"][
|
1368
|
+
"source-value"
|
1369
|
+
]
|
1370
|
+
|
1371
|
+
parameter_values = _get_optional_source_value(
|
1372
|
+
crystal_structure, "parameter-values"
|
1373
|
+
)
|
1374
|
+
|
1375
|
+
library_prototype_label = _get_optional_source_value(
|
1376
|
+
crystal_structure, "library-prototype-label"
|
1377
|
+
)
|
1378
|
+
|
1379
|
+
short_name = _get_optional_source_value(crystal_structure, "short-name")
|
1380
|
+
|
1381
|
+
# stress and temperature are always there (default 0), but we don't always write
|
1382
|
+
if write_stress:
|
1383
|
+
cell_cauchy_stress = crystal_structure["cell-cauchy-stress"]["source-value"]
|
1384
|
+
else:
|
1385
|
+
cell_cauchy_stress = None
|
1386
|
+
|
1387
|
+
cell_cauchy_stress_unit = crystal_structure["cell-cauchy-stress"]["source-unit"]
|
1388
|
+
|
1389
|
+
if write_temp:
|
1390
|
+
temperature = crystal_structure["temperature"]["source-value"]
|
1391
|
+
else:
|
1392
|
+
temperature = None
|
1393
|
+
|
1394
|
+
temperature_unit = crystal_structure["temperature"]["source-unit"]
|
1395
|
+
|
1396
|
+
crystal_genome_source_structure_id = _get_optional_source_value(
|
1397
|
+
crystal_structure, "crystal-genome-source-structure-id"
|
1398
|
+
)
|
1399
|
+
|
1400
|
+
super()._set_serialized_property_instances(
|
1401
|
+
_add_property_instance_and_common_crystal_genome_keys(
|
1402
|
+
property_name=property_name,
|
1403
|
+
prototype_label=prototype_label,
|
1404
|
+
stoichiometric_species=stoichiometric_species,
|
1405
|
+
a=a,
|
1406
|
+
parameter_values=parameter_values,
|
1407
|
+
library_prototype_label=library_prototype_label,
|
1408
|
+
short_name=short_name,
|
1409
|
+
cell_cauchy_stress=cell_cauchy_stress,
|
1410
|
+
temperature=temperature,
|
1411
|
+
crystal_genome_source_structure_id=crystal_genome_source_structure_id,
|
1412
|
+
a_unit=a_unit,
|
1413
|
+
cell_cauchy_stress_unit=cell_cauchy_stress_unit,
|
1414
|
+
temperature_unit=temperature_unit,
|
1415
|
+
disclaimer=disclaimer,
|
1416
|
+
property_instances=super()._get_serialized_property_instances(),
|
1417
|
+
)
|
1418
|
+
)
|
1419
|
+
|
1420
|
+
self.__add_poscar_to_curr_prop_inst(
|
1421
|
+
"primitive", "instance.poscar", "coordinates-file"
|
1422
|
+
)
|
1423
|
+
self.__add_poscar_to_curr_prop_inst(
|
1424
|
+
"conventional",
|
1425
|
+
"conventional.instance.poscar",
|
1426
|
+
"coordinates-file-conventional",
|
1427
|
+
)
|
1428
|
+
|
1429
|
+
def _get_temperature(self, unit: str = "K") -> float:
|
1430
|
+
"""
|
1431
|
+
Get the nominal temperature
|
1432
|
+
|
1433
|
+
Args:
|
1434
|
+
unit: The requested unit for the output. Must be understood by the GNU
|
1435
|
+
``units`` utility
|
1436
|
+
"""
|
1437
|
+
source_value = self.__nominal_crystal_structure_npt["temperature"][
|
1438
|
+
"source-value"
|
1439
|
+
]
|
1440
|
+
source_unit = self.__nominal_crystal_structure_npt["temperature"]["source-unit"]
|
1441
|
+
if source_unit != unit:
|
1442
|
+
temp = convert_units(source_value, source_unit, unit, True)
|
1443
|
+
else:
|
1444
|
+
temp = source_value
|
1445
|
+
return temp
|
1446
|
+
|
1447
|
+
def _get_cell_cauchy_stress(self, unit: str = "eV/angstrom^3") -> List[float]:
|
1448
|
+
"""
|
1449
|
+
Get the nominal stress
|
1450
|
+
|
1451
|
+
Args:
|
1452
|
+
unit: The requested unit for the output. Must be understood by the GNU
|
1453
|
+
``units`` utility
|
1454
|
+
"""
|
1455
|
+
source_value = self.__nominal_crystal_structure_npt["cell-cauchy-stress"][
|
1456
|
+
"source-value"
|
1457
|
+
]
|
1458
|
+
source_unit = self.__nominal_crystal_structure_npt["cell-cauchy-stress"][
|
1459
|
+
"source-unit"
|
1460
|
+
]
|
1461
|
+
if source_unit != unit:
|
1462
|
+
stress, _ = convert_list(source_value, source_unit, unit)
|
1463
|
+
else:
|
1464
|
+
stress = source_value
|
1465
|
+
return stress
|
1466
|
+
|
1467
|
+
def _get_mass_density(self, unit: str = "amu/angstrom^3") -> float:
|
1468
|
+
"""
|
1469
|
+
Get the mass density of the current nominal state of the system,
|
1470
|
+
according to the masses defined in :data:`ase.data.atomic_masses`
|
1471
|
+
|
1472
|
+
Args:
|
1473
|
+
unit:
|
1474
|
+
The requested units
|
1475
|
+
|
1476
|
+
Returns:
|
1477
|
+
The mass density of the crystal
|
1478
|
+
"""
|
1479
|
+
atoms = self._get_atoms() # always in angstrom
|
1480
|
+
vol_ang3 = atoms.get_volume()
|
1481
|
+
mass_amu = 0.0
|
1482
|
+
for atomic_number in atoms.get_atomic_numbers():
|
1483
|
+
mass_amu += atomic_masses[atomic_number]
|
1484
|
+
density_amu_ang3 = mass_amu / vol_ang3
|
1485
|
+
if unit != "amu/angstrom^3":
|
1486
|
+
density = convert_units(density_amu_ang3, "amu/angstrom^3", unit, True)
|
1487
|
+
else:
|
1488
|
+
density = density_amu_ang3
|
1489
|
+
|
1490
|
+
return density
|
1491
|
+
|
1492
|
+
def _get_nominal_crystal_structure_npt(self) -> Dict:
|
1493
|
+
"""
|
1494
|
+
Get the dictionary returning the current nominal state of the system.
|
1495
|
+
|
1496
|
+
Returns:
|
1497
|
+
An instance of the
|
1498
|
+
`crystal-structure-npt
|
1499
|
+
<https://openkim.org/properties/show/crystal-structure-npt>`_
|
1500
|
+
OpenKIM property containing a symmetry-reduced description of the nominal
|
1501
|
+
crystal structure.
|
1502
|
+
"""
|
1503
|
+
return self.__nominal_crystal_structure_npt
|
1504
|
+
|
1505
|
+
def deduplicate_property_instances(
|
1506
|
+
self,
|
1507
|
+
properties_to_deduplicate: Optional[List[str]] = None,
|
1508
|
+
allow_rotation: bool = True,
|
1509
|
+
) -> None:
|
1510
|
+
"""
|
1511
|
+
In the internally stored property instances,
|
1512
|
+
deduplicate any repeated crystal structures for each property id and merge
|
1513
|
+
their "crystal-genome-source-structure-id" keys.
|
1514
|
+
|
1515
|
+
WARNING: Only the crystal structures are checked. If you for some reason have a
|
1516
|
+
property that can reasonably report different non-structural values for the
|
1517
|
+
same atomic configuration, this will delete the extras!
|
1518
|
+
|
1519
|
+
Args:
|
1520
|
+
properties_to_deduplicate:
|
1521
|
+
A list of property names to pick out of ``property_instances`` to
|
1522
|
+
deduplicate. Each element can be the long or short name, e.g.
|
1523
|
+
"tag:staff@noreply.openkim.org,2023-02-21:property/binding-energy-crystal"
|
1524
|
+
or "binding-energy-crystal". If omitted, all properties will be
|
1525
|
+
deduplicated.
|
1526
|
+
allow_rotation:
|
1527
|
+
Whether or not structures that are rotated by a rotation that is not in
|
1528
|
+
the crystal's point group are considered identical
|
1529
|
+
"""
|
1530
|
+
deduplicated_property_instances = get_deduplicated_property_instances(
|
1531
|
+
self.property_instances, properties_to_deduplicate, allow_rotation
|
1532
|
+
)
|
1533
|
+
logger.info(
|
1534
|
+
f"Deduplicated {len(self.property_instances)} Property Instances "
|
1535
|
+
f"down to {len(deduplicated_property_instances)}."
|
1536
|
+
)
|
1537
|
+
# Remove files
|
1538
|
+
for original_property_instance in self.property_instances:
|
1539
|
+
instance_id = original_property_instance["instance-id"]
|
1540
|
+
instance_id_still_there = False
|
1541
|
+
for deduplicated_property_instance in deduplicated_property_instances:
|
1542
|
+
if instance_id == deduplicated_property_instance["instance-id"]:
|
1543
|
+
instance_id_still_there = True
|
1544
|
+
break
|
1545
|
+
if not instance_id_still_there:
|
1546
|
+
for key in original_property_instance:
|
1547
|
+
value_dict = original_property_instance[key]
|
1548
|
+
if not isinstance(value_dict, dict):
|
1549
|
+
continue
|
1550
|
+
if "source-unit" in value_dict:
|
1551
|
+
continue
|
1552
|
+
value = value_dict["source-value"]
|
1553
|
+
if isinstance(value, str):
|
1554
|
+
# TODO: should really check the property def that this is a
|
1555
|
+
# "file" type key, but come on, how incredibly unlikely is it
|
1556
|
+
# that there just happends to be a string type property that
|
1557
|
+
# happens to have a valid file path in it
|
1558
|
+
candidate_filename = os.path.join("output", value)
|
1559
|
+
if os.path.isfile(candidate_filename):
|
1560
|
+
os.remove(candidate_filename)
|
1561
|
+
|
1562
|
+
super()._set_serialized_property_instances(
|
1563
|
+
kim_edn.dumps(deduplicated_property_instances)
|
1564
|
+
)
|
1565
|
+
|
1566
|
+
def _set_serialized_property_instances(self, property_instances) -> None:
|
1567
|
+
"""
|
1568
|
+
An override to prevent SingleCrystalTestDriver derived classes from setting
|
1569
|
+
property instances directly
|
1570
|
+
"""
|
1571
|
+
raise NotImplementedError(
|
1572
|
+
"Setting property instances directly not supported "
|
1573
|
+
"in Crystal Genome Test Drivers"
|
1574
|
+
)
|
1575
|
+
|
1576
|
+
def _get_atoms(self, change_of_basis: Union[str, ArrayLike] = "primitive") -> Atoms:
|
1577
|
+
"""
|
1578
|
+
Get the atomic configuration representing the nominal crystal,
|
1579
|
+
with a calculator already attached.
|
1580
|
+
|
1581
|
+
Args:
|
1582
|
+
change_of_basis:
|
1583
|
+
Specify the desired unit cell. The default, ``"primitive"``, gives
|
1584
|
+
the cell as defined in the `AFLOW prototype standard
|
1585
|
+
<http://doi.org/10.1016/j.commatsci.2017.01.017>`_. ``"conventional"``
|
1586
|
+
gives the conventional cell defined therein.
|
1587
|
+
|
1588
|
+
Alternatively, provide an arbitrary change of basis matrix **P** as
|
1589
|
+
defined in ITA 1.5.1.2, with the above-defined primitive cell
|
1590
|
+
corresponding to the "old basis" and the returned ``Atoms`` object being
|
1591
|
+
in the "new basis".
|
1592
|
+
|
1593
|
+
See the docstring for :func:`kim_tools.change_of_basis_atoms` for
|
1594
|
+
more information on how to define the change of basis.
|
1595
|
+
|
1596
|
+
Returns:
|
1597
|
+
Unit cell of the crystal.
|
1598
|
+
Lengths are always in angstrom
|
1599
|
+
"""
|
1600
|
+
crystal_structure = self.__nominal_crystal_structure_npt
|
1601
|
+
atoms_prim = get_atoms_from_crystal_structure(crystal_structure)
|
1602
|
+
if isinstance(change_of_basis, str):
|
1603
|
+
if change_of_basis.lower() == "primitive":
|
1604
|
+
change_of_basis_matrix = None
|
1605
|
+
elif change_of_basis.lower() == "conventional":
|
1606
|
+
prototype_label = crystal_structure["prototype-label"]["source-value"]
|
1607
|
+
sgnum = get_space_group_number_from_prototype(prototype_label)
|
1608
|
+
formal_bravais_lattice = get_formal_bravais_lattice_from_space_group(
|
1609
|
+
sgnum
|
1610
|
+
)
|
1611
|
+
change_of_basis_matrix = get_change_of_basis_matrix_to_conventional_cell_from_formal_bravais_lattice( # noqa: E501
|
1612
|
+
formal_bravais_lattice
|
1613
|
+
)
|
1614
|
+
else:
|
1615
|
+
raise KIMTestDriverError(
|
1616
|
+
'Allowable string values for `change_of_basis` are "primitive" or '
|
1617
|
+
f'"conventional". You provided f{change_of_basis}'
|
1618
|
+
)
|
1619
|
+
else:
|
1620
|
+
change_of_basis_matrix = change_of_basis
|
1621
|
+
|
1622
|
+
if change_of_basis_matrix is None:
|
1623
|
+
atoms_tmp = atoms_prim
|
1624
|
+
else:
|
1625
|
+
atoms_tmp = change_of_basis_atoms(atoms_prim, change_of_basis_matrix)
|
1626
|
+
|
1627
|
+
atoms_tmp.calc = self._calc
|
1628
|
+
return atoms_tmp
|
1629
|
+
|
1630
|
+
|
1631
|
+
def query_crystal_structures(
|
1632
|
+
stoichiometric_species: List[str],
|
1633
|
+
prototype_label: Optional[str] = None,
|
1634
|
+
short_name: Optional[str] = None,
|
1635
|
+
cell_cauchy_stress_eV_angstrom3: Optional[List[float]] = None,
|
1636
|
+
temperature_K: float = 0,
|
1637
|
+
kim_model_name: Optional[str] = None,
|
1638
|
+
) -> List[Dict]:
|
1639
|
+
"""
|
1640
|
+
Query for all equilibrium parameter sets for this species combination and,
|
1641
|
+
optionally, crystal structure specified by ``prototype_label`` and/or ``short_name``
|
1642
|
+
in the KIM database. This is a utility function for running the test outside of the
|
1643
|
+
OpenKIM pipeline. In the OpenKIM pipeline, this information is delivered to the test
|
1644
|
+
driver through the ``runner`` script.
|
1645
|
+
|
1646
|
+
Args:
|
1647
|
+
stoichiometric_species:
|
1648
|
+
List of unique species in the crystal. Required part of the Crystal Genome
|
1649
|
+
designation.
|
1650
|
+
short_name:
|
1651
|
+
short name of the crystal, e.g. "Hexagonal Close Packed". This will be
|
1652
|
+
searched as a case-insensitive regex, so partial matches will be returned.
|
1653
|
+
The list of possible shortnames is taken by postprocessing README_PROTO.TXT
|
1654
|
+
from the AFLOW software and packaged with kim-tools for reproducibility. To
|
1655
|
+
see the exact list of possible short names, call
|
1656
|
+
:func:`kim_tools.read_shortnames` and inspect the values of the returned
|
1657
|
+
dictionary. Note that a given short name corresponds to an exact set of
|
1658
|
+
parameters (with some tolerance), except the overall scale of the crystal.
|
1659
|
+
For example, "Hexagonal Close Packed" will return only structures with a
|
1660
|
+
c/a close to 1.63 (actually close packed), not any structure with the same
|
1661
|
+
symmetry as HCP as is sometimes colloquially understood.
|
1662
|
+
TODO: consider adding the same expanded logic we have on the website that
|
1663
|
+
searches for any crystal with the symmetry corresponding to a given
|
1664
|
+
shortname, i.e. invalidating the last caveat given above.
|
1665
|
+
prototype_label:
|
1666
|
+
AFLOW prototype label for the crystal.
|
1667
|
+
cell_cauchy_stress_eV_angstrom3:
|
1668
|
+
Cauchy stress on the cell in eV/angstrom^3 (ASE units) in
|
1669
|
+
[xx,yy,zz,yz,xz,xy] format
|
1670
|
+
temperature_K:
|
1671
|
+
The temperature in Kelvin
|
1672
|
+
kim_model_name:
|
1673
|
+
KIM model name. If not provided, RD will be queried instead
|
1674
|
+
|
1675
|
+
Returns:
|
1676
|
+
List of kim property instances matching the query in the OpenKIM.org database
|
1677
|
+
"""
|
1678
|
+
if cell_cauchy_stress_eV_angstrom3 is None:
|
1679
|
+
cell_cauchy_stress_eV_angstrom3 = [0, 0, 0, 0, 0, 0]
|
1680
|
+
|
1681
|
+
stoichiometric_species.sort()
|
1682
|
+
|
1683
|
+
# TODO: Some kind of generalized query interface for all tests, this is very
|
1684
|
+
# hand-made
|
1685
|
+
cell_cauchy_stress_Pa = [
|
1686
|
+
component * 1.6021766e11 for component in cell_cauchy_stress_eV_angstrom3
|
1687
|
+
]
|
1688
|
+
|
1689
|
+
query = {
|
1690
|
+
"meta.type": "rd" if kim_model_name is None else "tr",
|
1691
|
+
"property-id": (
|
1692
|
+
"tag:staff@noreply.openkim.org,2023-02-21:property/crystal-structure-npt"
|
1693
|
+
),
|
1694
|
+
"stoichiometric-species.source-value": {
|
1695
|
+
"$size": len(stoichiometric_species),
|
1696
|
+
"$all": stoichiometric_species,
|
1697
|
+
},
|
1698
|
+
"cell-cauchy-stress.si-value": cell_cauchy_stress_Pa,
|
1699
|
+
"temperature.si-value": temperature_K,
|
1700
|
+
}
|
1701
|
+
|
1702
|
+
if kim_model_name is not None:
|
1703
|
+
query["meta.subject.extended-id"] = kim_model_name
|
1704
|
+
|
1705
|
+
if prototype_label is not None:
|
1706
|
+
query["prototype-label.source-value"] = prototype_label
|
1707
|
+
|
1708
|
+
if short_name is not None:
|
1709
|
+
query["short-name.source-value"] = {"$regex": short_name, "$options": "$i"}
|
1710
|
+
|
1711
|
+
raw_query_args = {
|
1712
|
+
"query": query,
|
1713
|
+
"database": "data",
|
1714
|
+
"limit": 0,
|
1715
|
+
}
|
1716
|
+
|
1717
|
+
logger.info(f"Sending below query:\n{raw_query_args}")
|
1718
|
+
|
1719
|
+
query_result = raw_query(**raw_query_args)
|
1720
|
+
|
1721
|
+
len_msg = (
|
1722
|
+
f"Found {len(query_result)} equilibrium structures from "
|
1723
|
+
"query_crystal_genome_structures()"
|
1724
|
+
)
|
1725
|
+
logger.info(len_msg)
|
1726
|
+
logger.debug(f"Query result (length={len(query_result)}):\n{query_result}")
|
1727
|
+
|
1728
|
+
print(f"!!! {len_msg} !!!")
|
1729
|
+
|
1730
|
+
return query_result
|
1731
|
+
|
1732
|
+
|
1733
|
+
def detect_unique_crystal_structures(
|
1734
|
+
crystal_structures: Union[List[Dict], Dict],
|
1735
|
+
allow_rotation: bool = True,
|
1736
|
+
aflow_np: int = 4,
|
1737
|
+
) -> Dict:
|
1738
|
+
"""
|
1739
|
+
Detect which of the provided crystal structures is unique
|
1740
|
+
|
1741
|
+
Args:
|
1742
|
+
crystal_structures:
|
1743
|
+
A list of dictionaries in KIM Property format, each containing the Crystal
|
1744
|
+
Genome keys required to build a structure, namely: "stoichiometric-species",
|
1745
|
+
"prototype-label", "a", and, if the prototype has free parameters,
|
1746
|
+
"parameter-values". These dictionaries are not required to be complete KIM
|
1747
|
+
Property Instances, e.g. the keys "property-id", "instance-id" and "meta"
|
1748
|
+
can be absent. Alternatively, this can be a dictionary of dictionaries with
|
1749
|
+
integers as indices, for the recursive call.
|
1750
|
+
allow_rotation:
|
1751
|
+
Whether or not structures that are rotated by a rotation that is not in the
|
1752
|
+
crystal's point group are considered identical
|
1753
|
+
Returns:
|
1754
|
+
Dictionary with keys corresponding to indices of unique structures and values
|
1755
|
+
being lists of indices of their duplicates
|
1756
|
+
"""
|
1757
|
+
if len(crystal_structures) == 0:
|
1758
|
+
return []
|
1759
|
+
|
1760
|
+
aflow = AFLOW(np=aflow_np)
|
1761
|
+
|
1762
|
+
with TemporaryDirectory() as tmpdirname:
|
1763
|
+
# I don't know if crystal_structurs is a list or a dict with integer keys
|
1764
|
+
for i in (
|
1765
|
+
range(len(crystal_structures))
|
1766
|
+
if isinstance(crystal_structures, list)
|
1767
|
+
else crystal_structures
|
1768
|
+
):
|
1769
|
+
structure = crystal_structures[i]
|
1770
|
+
try:
|
1771
|
+
get_poscar_from_crystal_structure(
|
1772
|
+
structure, os.path.join(tmpdirname, str(i))
|
1773
|
+
)
|
1774
|
+
except AFLOW.ChangedSymmetryException:
|
1775
|
+
logger.info(
|
1776
|
+
f"Comparison structure {i} failed to write a POSCAR due to a "
|
1777
|
+
"detected higher symmetry"
|
1778
|
+
)
|
1779
|
+
|
1780
|
+
comparison = aflow.compare_materials_dir(tmpdirname)
|
1781
|
+
|
1782
|
+
unique_materials = {}
|
1783
|
+
for materials_group in comparison:
|
1784
|
+
i_repr = int(
|
1785
|
+
materials_group["structure_representative"]["name"].split("/")[-1]
|
1786
|
+
)
|
1787
|
+
unique_materials[i_repr] = []
|
1788
|
+
for structure_duplicate in materials_group["structures_duplicate"]:
|
1789
|
+
unique_materials[i_repr].append(
|
1790
|
+
int(structure_duplicate["name"].split("/")[-1])
|
1791
|
+
)
|
1792
|
+
|
1793
|
+
if not allow_rotation:
|
1794
|
+
for materials_group in comparison:
|
1795
|
+
# to preserve their ordering in the original input list, make this a
|
1796
|
+
# dictionary now
|
1797
|
+
repr_filename = materials_group["structure_representative"]["name"]
|
1798
|
+
rotated_structures = {}
|
1799
|
+
cell = get_cell_from_poscar(repr_filename)
|
1800
|
+
sgnum = materials_group["space_group"]
|
1801
|
+
for potential_rotated_duplicate in materials_group[
|
1802
|
+
"structures_duplicate"
|
1803
|
+
]:
|
1804
|
+
cart_rot = potential_rotated_duplicate["rotation"]
|
1805
|
+
if not cartesian_rotation_is_in_point_group(cart_rot, sgnum, cell):
|
1806
|
+
i_rot_dup = int(
|
1807
|
+
potential_rotated_duplicate["name"].split("/")[-1]
|
1808
|
+
)
|
1809
|
+
rotated_structures[i_rot_dup] = crystal_structures[i_rot_dup]
|
1810
|
+
# Now that we know it's rotated, need to remove
|
1811
|
+
# it from the list of duplicates
|
1812
|
+
i_repr = int(repr_filename.split("/")[-1])
|
1813
|
+
unique_materials[i_repr].remove(i_rot_dup)
|
1814
|
+
|
1815
|
+
unique_materials.update(
|
1816
|
+
detect_unique_crystal_structures(
|
1817
|
+
rotated_structures, False, aflow_np
|
1818
|
+
)
|
1819
|
+
)
|
1820
|
+
|
1821
|
+
return unique_materials
|
1822
|
+
|
1823
|
+
|
1824
|
+
def get_deduplicated_property_instances(
|
1825
|
+
property_instances: List[Dict],
|
1826
|
+
properties_to_deduplicate: Optional[List[str]] = None,
|
1827
|
+
allow_rotation: bool = True,
|
1828
|
+
) -> List[Dict]:
|
1829
|
+
"""
|
1830
|
+
Given a list of dictionaries constituting KIM Property instances,
|
1831
|
+
deduplicate any repeated crystal structures for each property id and merge
|
1832
|
+
their "crystal-genome-source-structure-id" keys.
|
1833
|
+
|
1834
|
+
WARNING: Only the crystal structures are checked. If you for some reason have a
|
1835
|
+
property that can reasonably report different non-structural values for the
|
1836
|
+
same atomic configuration, this will delete the extras!
|
1837
|
+
|
1838
|
+
Args:
|
1839
|
+
property_instances:
|
1840
|
+
The list of KIM Property Instances to deduplicate
|
1841
|
+
properties_to_deduplicate:
|
1842
|
+
A list of property names to pick out of ``property_instances`` to
|
1843
|
+
deduplicate. Each element can be the long or short name, e.g.
|
1844
|
+
"tag:staff@noreply.openkim.org,2023-02-21:property/binding-energy-crystal"
|
1845
|
+
or "binding-energy-crystal". If omitted, all properties will be
|
1846
|
+
deduplicated.
|
1847
|
+
allow_rotation:
|
1848
|
+
Whether or not structures that are rotated by a rotation that is not in the
|
1849
|
+
crystal's point group are considered identical
|
1850
|
+
|
1851
|
+
Returns:
|
1852
|
+
The deduplicated property instances
|
1853
|
+
"""
|
1854
|
+
if properties_to_deduplicate is None:
|
1855
|
+
properties_set = set()
|
1856
|
+
for property_instance in property_instances:
|
1857
|
+
properties_set.add(property_instance["property-id"])
|
1858
|
+
properties_to_deduplicate = list(properties_set)
|
1859
|
+
|
1860
|
+
property_instances_deduplicated = []
|
1861
|
+
for property_name in properties_to_deduplicate:
|
1862
|
+
# Pick out property instances with the relevant name
|
1863
|
+
property_instances_curr_name = []
|
1864
|
+
for property_instance in property_instances:
|
1865
|
+
property_id = property_instance["property-id"]
|
1866
|
+
if (
|
1867
|
+
property_id == property_name
|
1868
|
+
or get_property_id_path(property_id)[3] == property_name
|
1869
|
+
):
|
1870
|
+
property_instances_curr_name.append(deepcopy(property_instance))
|
1871
|
+
if len(property_instances_curr_name) == 0:
|
1872
|
+
raise KIMTestDriverError(
|
1873
|
+
"The property you asked to deduplicate "
|
1874
|
+
"is not in the property instances you provided"
|
1875
|
+
)
|
1876
|
+
|
1877
|
+
# Get unique-duplicate dictionary
|
1878
|
+
unique_crystal_structures = detect_unique_crystal_structures(
|
1879
|
+
property_instances_curr_name, allow_rotation
|
1880
|
+
)
|
1881
|
+
|
1882
|
+
# Put together the list of unique instances for the current
|
1883
|
+
# name only
|
1884
|
+
property_instances_curr_name_deduplicated = []
|
1885
|
+
for i_unique in unique_crystal_structures:
|
1886
|
+
property_instances_curr_name_deduplicated.append(
|
1887
|
+
property_instances_curr_name[i_unique]
|
1888
|
+
)
|
1889
|
+
# Put together a list of "crystal-genome-source-structure-id"
|
1890
|
+
# to gather into the deduplicated structure
|
1891
|
+
additional_source_structure_id = []
|
1892
|
+
for i_dup in unique_crystal_structures[i_unique]:
|
1893
|
+
source_structure_id = _get_optional_source_value(
|
1894
|
+
property_instances_curr_name[i_dup],
|
1895
|
+
"crystal-genome-source-structure-id",
|
1896
|
+
)
|
1897
|
+
if source_structure_id is not None:
|
1898
|
+
# "crystal-genome-source-structure-id" is a 2D list
|
1899
|
+
# but only the first row should be populated
|
1900
|
+
additional_source_structure_id += source_structure_id[0]
|
1901
|
+
|
1902
|
+
if len(additional_source_structure_id) != 0:
|
1903
|
+
if (
|
1904
|
+
"crystal-genome-source-structure-id"
|
1905
|
+
not in property_instances_curr_name_deduplicated[-1]
|
1906
|
+
):
|
1907
|
+
property_instances_curr_name_deduplicated[-1][
|
1908
|
+
"crystal-genome-source-structure-id"
|
1909
|
+
] = [[]]
|
1910
|
+
property_instances_curr_name_deduplicated[-1][
|
1911
|
+
"crystal-genome-source-structure-id"
|
1912
|
+
]["source-value"][0] += additional_source_structure_id
|
1913
|
+
|
1914
|
+
property_instances_deduplicated += property_instances_curr_name_deduplicated
|
1915
|
+
|
1916
|
+
# Add any instances of properties that weren't deduplicated
|
1917
|
+
for property_instance in property_instances:
|
1918
|
+
property_id = property_instance["property-id"]
|
1919
|
+
if (
|
1920
|
+
property_id not in properties_to_deduplicate
|
1921
|
+
and get_property_id_path(property_id)[3] not in properties_to_deduplicate
|
1922
|
+
):
|
1923
|
+
property_instances_deduplicated.append(deepcopy(property_instance))
|
1924
|
+
|
1925
|
+
property_instances_deduplicated.sort(key=lambda a: a["instance-id"])
|
1926
|
+
|
1927
|
+
return property_instances_deduplicated
|
1928
|
+
|
1929
|
+
|
1930
|
+
# If called directly, do nothing
|
1931
|
+
if __name__ == "__main__":
|
1932
|
+
pass
|