kmcpy 0.1.0__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.
- kmcpy/__init__.py +64 -0
- kmcpy/_version.py +3 -0
- kmcpy/event.py +173 -0
- kmcpy/event_generator.py +922 -0
- kmcpy/fitting.py +215 -0
- kmcpy/io.py +339 -0
- kmcpy/kmc.py +416 -0
- kmcpy/model.py +465 -0
- kmcpy/tracker.py +535 -0
- kmcpy-0.1.0.dist-info/METADATA +149 -0
- kmcpy-0.1.0.dist-info/RECORD +14 -0
- kmcpy-0.1.0.dist-info/WHEEL +5 -0
- kmcpy-0.1.0.dist-info/licenses/LICENSE +21 -0
- kmcpy-0.1.0.dist-info/top_level.txt +1 -0
kmcpy/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Main kMCpy module."""
|
|
2
|
+
import datetime
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from ._version import __version__
|
|
7
|
+
|
|
8
|
+
__author__ = "Zeyu Deng"
|
|
9
|
+
__author_email__ = "dengzeyu@gmail.com"
|
|
10
|
+
|
|
11
|
+
# Expose the version and the logo function as the public API for this module.
|
|
12
|
+
__all__ = ["__version__", "get_logo"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# 2. LOGGING SETUP: This is perfect.
|
|
16
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# 3. LOGO FUNCTION: A stateless function to generate and print the logo.
|
|
20
|
+
|
|
21
|
+
def get_logo():
|
|
22
|
+
"""
|
|
23
|
+
Prints the kMCpy ASCII logo and version/citation information to the console.
|
|
24
|
+
|
|
25
|
+
This function can be called multiple times. It's recommended to call
|
|
26
|
+
it once at the start of an application's main entry point.
|
|
27
|
+
"""
|
|
28
|
+
# Ansi color codes for a professional look.
|
|
29
|
+
class Colors:
|
|
30
|
+
CYAN = '\033[96m'
|
|
31
|
+
YELLOW = '\033[93m'
|
|
32
|
+
BOLD = '\033[1m'
|
|
33
|
+
END = '\033[0m'
|
|
34
|
+
|
|
35
|
+
# The logo art. Using a raw string (r"...") is good practice.
|
|
36
|
+
logo_art = r"""
|
|
37
|
+
_ __ __ _____
|
|
38
|
+
| | | \/ |/ ____|
|
|
39
|
+
| | _| \ / | | _ __ _ _
|
|
40
|
+
| |/ / |\/| | | | '_ \| | | |
|
|
41
|
+
| <| | | | |____| |_) | |_| |
|
|
42
|
+
|_|\_\_| |_|\_____| .__/ \__, |
|
|
43
|
+
| | __/ |
|
|
44
|
+
|_| |___/
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
# Using os.uname() is not cross-platform (it fails on Windows).
|
|
48
|
+
# platform.node() is a more robust alternative.
|
|
49
|
+
import platform
|
|
50
|
+
hostname = platform.node()
|
|
51
|
+
|
|
52
|
+
# Assemble the final message string inside the function.
|
|
53
|
+
# This ensures all information (like datetime) is current.
|
|
54
|
+
message = (
|
|
55
|
+
f"{Colors.CYAN}{Colors.BOLD}{logo_art}{Colors.END}"
|
|
56
|
+
f"kMCpy: A python package to simulate transport properties in solids with kinetic Monte Carlo\n"
|
|
57
|
+
f"{Colors.YELLOW}Please cite:{Colors.END} DOI: 10.1016/j.commatsci.2023.112394 and 10.1038/s41467-022-32190-7 \n"
|
|
58
|
+
f"{Colors.YELLOW}Author:{Colors.END} {__author__} <{__author_email__}>\n"
|
|
59
|
+
f"Version: {__version__}\n"
|
|
60
|
+
f"Timestamp: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
|
61
|
+
f"Host: {hostname}\n"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return message
|
kmcpy/_version.py
ADDED
kmcpy/event.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
This module defines the Event class, which encapsulates the information and methods required to represent and analyze a migration event in a lattice-based simulation, such as those used in kinetic Monte Carlo (kMC) studies. The Event class manages indices of mobile ions, local environments, sublattice information, and provides methods for initializing, updating, and serializing event data. It also includes methods for calculating occupation, correlation, migration barriers, and probabilities associated with migration events.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import numba as nb
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
import json
|
|
10
|
+
from kmcpy.io import convert
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
logging.getLogger('numba').setLevel(logging.WARNING)
|
|
15
|
+
|
|
16
|
+
class Event:
|
|
17
|
+
"""
|
|
18
|
+
mobile_ion_specie_1_index
|
|
19
|
+
mobile_ion_specie_2_index
|
|
20
|
+
local_env_indices_list
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
def initialization(
|
|
27
|
+
self,
|
|
28
|
+
mobile_ion_specie_1_index=12,
|
|
29
|
+
mobile_ion_specie_2_index=15,
|
|
30
|
+
local_env_indices_list=[1, 2, 3, 4, 5],
|
|
31
|
+
):
|
|
32
|
+
"""3rd version of initialization. The input local_env_indices_list is already sorted. Center atom is equivalent to the Na1 in the 1st version and mobile_ion_specie_2_index is equivalent to the Na2 in the 1st version
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
mobile_ion_specie_1_index (int, optional): the global index (index in supercell) of the center atom. Defaults to 12.
|
|
36
|
+
mobile_ion_specie_2_index (int, optional): the global index of the atom that the center atom is about to diffuse to. Defaults to 15.
|
|
37
|
+
local_env_indices_list (list, optional): list of integers, which is a list of indices of the neighboring sites in supercell, and is already sorted. Defaults to [1,2,3,4,5].
|
|
38
|
+
"""
|
|
39
|
+
self.mobile_ion_specie_1_index = mobile_ion_specie_1_index
|
|
40
|
+
self.mobile_ion_specie_2_index = mobile_ion_specie_2_index
|
|
41
|
+
|
|
42
|
+
self.local_env_indices_list = local_env_indices_list
|
|
43
|
+
self.local_env_indices_list_site = local_env_indices_list
|
|
44
|
+
|
|
45
|
+
def set_sublattice_indices(self, sublattice_indices, sublattice_indices_site):
|
|
46
|
+
self.sublattice_indices = sublattice_indices # this stores the site indices from local_cluster_expansion object
|
|
47
|
+
self.sublattice_indices_site = sublattice_indices_site # this stores the site indices from local_cluster_expansion object
|
|
48
|
+
|
|
49
|
+
def show_info(self):
|
|
50
|
+
logger.info(
|
|
51
|
+
"Event: mobile_ion(1)[%s]<--> mobile_ion(2)[%s]",
|
|
52
|
+
self.mobile_ion_specie_1_index,
|
|
53
|
+
self.mobile_ion_specie_2_index,
|
|
54
|
+
)
|
|
55
|
+
logger.debug('Global sites indices are (excluding O and Zr): %s', self.local_env_indices_list)
|
|
56
|
+
logger.debug('Local template structure: %s', getattr(self, 'sorted_local_structure', None))
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
logger.info("occ_sublat\tE_KRA\tProbability")
|
|
60
|
+
logger.info("%s\t%s\t%s", self.occ_sublat, self.ekra, self.probability)
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def clear_property(self):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
def analyze_local_structure(self, local_env_info):
|
|
68
|
+
#
|
|
69
|
+
indices_sites_group = [(s["site_index"], s["site"]) for s in local_env_info]
|
|
70
|
+
|
|
71
|
+
# this line is to sort the neighbors. First sort by x coordinate, and then sort by specie (Na, then Si/P)
|
|
72
|
+
# the sorted list, store the sequence of hash.
|
|
73
|
+
# for other materials, need to find another method to sort.
|
|
74
|
+
# this sort only works for the NaSICON!
|
|
75
|
+
indices_sites_group_sorted = sorted(
|
|
76
|
+
sorted(indices_sites_group, key=lambda x: x[1].coords[0]),
|
|
77
|
+
key=lambda x: x[1].specie,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
local_env_indices_list = [s[0] for s in indices_sites_group_sorted]
|
|
81
|
+
return local_env_indices_list
|
|
82
|
+
|
|
83
|
+
# @profile
|
|
84
|
+
def set_occ(self, occ_global):
|
|
85
|
+
self.occ_sublat = deepcopy(
|
|
86
|
+
occ_global[self.local_env_indices_list]
|
|
87
|
+
) # occ is an 1D numpy array
|
|
88
|
+
|
|
89
|
+
# @profile
|
|
90
|
+
def initialize_corr(self):
|
|
91
|
+
self.corr = np.empty(shape=len(self.sublattice_indices))
|
|
92
|
+
self.corr_site = np.empty(shape=len(self.sublattice_indices_site))
|
|
93
|
+
|
|
94
|
+
# @profile
|
|
95
|
+
def set_corr(self):
|
|
96
|
+
_set_corr(self.corr, self.occ_sublat, self.sublattice_indices)
|
|
97
|
+
_set_corr(self.corr_site, self.occ_sublat, self.sublattice_indices_site)
|
|
98
|
+
|
|
99
|
+
# @profile
|
|
100
|
+
def set_ekra(
|
|
101
|
+
self, keci, empty_cluster, keci_site, empty_cluster_site
|
|
102
|
+
): # input is the keci and empty_cluster; ekra = corr*keci + empty_cluster
|
|
103
|
+
self.ekra = np.inner(self.corr, keci) + empty_cluster
|
|
104
|
+
self.esite = np.inner(self.corr_site, keci_site) + empty_cluster_site
|
|
105
|
+
|
|
106
|
+
# @profile
|
|
107
|
+
def set_probability(
|
|
108
|
+
self, occ_global, v, T
|
|
109
|
+
): # calc_probability() will evaluate migration probability for this event, should be updated everytime when change occupation
|
|
110
|
+
k = 8.617333262145 * 10 ** (-2) # unit in meV/K
|
|
111
|
+
direction = (
|
|
112
|
+
occ_global[self.mobile_ion_specie_2_index]
|
|
113
|
+
- occ_global[self.mobile_ion_specie_1_index]
|
|
114
|
+
) / 2 # 1 if na1 -> na2, -1 if na2 -> na1
|
|
115
|
+
self.barrier = self.ekra + direction * self.esite / 2 # ekra
|
|
116
|
+
self.probability = abs(direction) * v * np.exp(-1 * (self.barrier) / (k * T))
|
|
117
|
+
|
|
118
|
+
# @profile
|
|
119
|
+
def update_event(
|
|
120
|
+
self, occ_global, v, T, keci, empty_cluster, keci_site, empty_cluster_site
|
|
121
|
+
):
|
|
122
|
+
self.set_occ(occ_global) # change occupation and correlation for this unit
|
|
123
|
+
self.set_corr()
|
|
124
|
+
self.set_ekra(
|
|
125
|
+
keci, empty_cluster, keci_site, empty_cluster_site
|
|
126
|
+
) # calculate ekra and probability
|
|
127
|
+
self.set_probability(occ_global, v, T)
|
|
128
|
+
|
|
129
|
+
def as_dict(self):
|
|
130
|
+
d = {
|
|
131
|
+
"mobile_ion_specie_1_index": self.mobile_ion_specie_1_index,
|
|
132
|
+
"mobile_ion_specie_2_index": self.mobile_ion_specie_2_index,
|
|
133
|
+
"local_env_indices_list": self.local_env_indices_list,
|
|
134
|
+
"local_env_indices_list": self.local_env_indices_list,
|
|
135
|
+
"local_env_indices_list_site": self.local_env_indices_list_site,
|
|
136
|
+
}
|
|
137
|
+
return d
|
|
138
|
+
|
|
139
|
+
def to_json(self, fname):
|
|
140
|
+
logger.info("Saving: %s", fname)
|
|
141
|
+
with open(fname, "w") as fhandle:
|
|
142
|
+
d = self.as_dict()
|
|
143
|
+
jsonStr = json.dumps(
|
|
144
|
+
d, indent=4, default=convert
|
|
145
|
+
) # to get rid of errors of int64
|
|
146
|
+
fhandle.write(jsonStr)
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def from_json(self, fname):
|
|
150
|
+
logger.info("Loading: %s", fname)
|
|
151
|
+
with open(fname, "rb") as fhandle:
|
|
152
|
+
objDict = json.load(fhandle)
|
|
153
|
+
obj = Event()
|
|
154
|
+
obj.__dict__ = objDict
|
|
155
|
+
return obj
|
|
156
|
+
@classmethod
|
|
157
|
+
def from_dict(self, event_dict): # convert dict into event object
|
|
158
|
+
event = Event()
|
|
159
|
+
event.__dict__ = event_dict
|
|
160
|
+
return event
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@nb.njit
|
|
164
|
+
def _set_corr(corr, occ_latt, sublattice_indices):
|
|
165
|
+
i = 0
|
|
166
|
+
for sublat_ind_orbit in sublattice_indices:
|
|
167
|
+
corr[i] = 0
|
|
168
|
+
for sublat_ind_cluster in sublat_ind_orbit:
|
|
169
|
+
corr_cluster = 1
|
|
170
|
+
for occ_site in sublat_ind_cluster:
|
|
171
|
+
corr_cluster *= occ_latt[occ_site]
|
|
172
|
+
corr[i] += corr_cluster
|
|
173
|
+
i += 1
|