Skip to content

Commit 8806920

Browse files
committed
Allow atom types to be consistent across generations/iterations. Closes #9
1 parent 83f8495 commit 8806920

File tree

2 files changed

+65
-46
lines changed

2 files changed

+65
-46
lines changed

GBOpt/GBManipulator.py

Lines changed: 59 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ class Parent:
106106
:param unit_cell: Required only if GB is specified using a LAMMPS dump file. Gives
107107
the nominal unit cell of the system.
108108
:param gb_thickness: Thickness of the GB region, optional, defaults to 10.
109+
:param type_dict: The map from integer to element string. The default mapping is
110+
1 -> 'H', 2 -> 'He', etc.
109111
"""
110112
__num_to_name = {val: key for key, val in Atom._numbers.items()}
111113

@@ -115,7 +117,7 @@ def __init__(
115117
*,
116118
unit_cell: UnitCell = None,
117119
gb_thickness: float = 10,
118-
type_dict: dict = {}
120+
type_dict: dict = {},
119121
) -> None:
120122
if isinstance(system, GBMaker):
121123
self.__init_by_gbmaker(system)
@@ -139,10 +141,10 @@ def __init__(
139141
self.__GBpos = self.__whole_system[
140142
np.where(
141143
np.logical_and(
142-
self.__whole_system["x"] >= self.__box_dims[0, 1] /
143-
2 - self.__gb_thickness/2,
144-
self.__whole_system["x"] <= self.__box_dims[0, 1] /
145-
2 + self.__gb_thickness/2
144+
self.__whole_system["x"]
145+
>= self.__box_dims[0, 1] / 2 - self.__gb_thickness/2,
146+
self.__whole_system["x"]
147+
<= self.__box_dims[0, 1] / 2 + self.__gb_thickness/2
146148
)
147149
)
148150
]
@@ -171,7 +173,7 @@ def __init_by_file(
171173
system_file: str,
172174
unit_cell: UnitCell,
173175
gb_thickness: float,
174-
type_dict: dict
176+
type_dict: dict,
175177
) -> None:
176178
"""
177179
Method for initializing the Parent using a file.
@@ -211,7 +213,7 @@ def __init_by_file(
211213
"ITEM: TIMESTEP",
212214
"ITEM: NUMBER OF ATOMS",
213215
"ITEM: BOX BOUNDS",
214-
"ITEM: ATOMS"
216+
"ITEM: ATOMS",
215217
],
216218
self.__init_from_lammps_input: [
217219
"atoms",
@@ -231,7 +233,7 @@ def __init_by_file(
231233
"avec",
232234
"bvec",
233235
"cvec",
234-
"abc origin"
236+
"abc origin",
235237
]
236238
}
237239

@@ -354,7 +356,10 @@ def convert_type(value):
354356
if typelabel_in_attrs:
355357
return value
356358
else:
357-
return self.__num_to_name[int(value)]
359+
if not type_dict:
360+
return self.__num_to_name[int(value)]
361+
else:
362+
return type_dict[int(value)]
358363
max_rows = 0
359364
line = f.readline() # read the next line to move the file pointer ahead.
360365
while not line.startswith("ITEM"):
@@ -466,7 +471,7 @@ def convert_type(value):
466471
max_rows=n_atoms,
467472
converters={1: convert_type},
468473
usecols=[1, 2, 3, 4],
469-
dtype=Atom.atom_dtype
474+
dtype=Atom.atom_dtype,
470475
)
471476
mask = self.__whole_system["x"] < grain_cutoff
472477
self.__left_grain = self.__whole_system[mask]
@@ -605,7 +610,7 @@ def _calculate_fingerprint_vector(atom, neighs, NB, V, Btype, Delta, Rmax):
605610
calculate the fingerprint.
606611
:return: The vector containing the fingerprint for *atom*.
607612
"""
608-
Rs = np.arange(0, Rmax+Delta, Delta)
613+
Rs = np.arange(0, Rmax + Delta, Delta)
609614

610615
fingerprint_vector = np.zeros_like(Rs)
611616
for idx, R in enumerate(Rs):
@@ -615,10 +620,9 @@ def _calculate_fingerprint_vector(atom, neighs, NB, V, Btype, Delta, Rmax):
615620
diff = atom[1:] - neigh[1:]
616621
# Rij = np.linalg.norm(atom[1:] - neigh[1:])
617622
distance = np.sqrt(np.dot(diff, diff))
618-
delta = _gaussian(R-distance, 0.02)
623+
delta = _gaussian(R - distance, 0.02)
619624
local_sum += delta / \
620625
(4 * np.pi * distance * distance * (NB / V) * Delta)
621-
# pdb.set_trace()
622626
fingerprint_vector[idx] = local_sum - 1
623627

624628
return fingerprint_vector
@@ -642,8 +646,8 @@ def _calculate_local_order(atom, neighs, unit_cell_types, unit_cell_a0, N, Delta
642646
"""
643647
local_sum = 0
644648
atom_types = np.unique(neighs[:, 0])
645-
V = unit_cell_a0**3
646-
prefactor = Delta / (N * (V/N)**(1/3))
649+
V = unit_cell_a0 ** 3
650+
prefactor = Delta / (N * (V / N) ** (1 / 3))
647651
for Btype in atom_types:
648652
NB = np.sum(unit_cell_types == Btype)
649653
fingerprint = _calculate_fingerprint_vector(
@@ -793,6 +797,8 @@ class GBManipulator:
793797
:param gb_thickness: Thickness of the GB region, optional, defaults to 10.
794798
:param seed: The seed for random number generation, optional, defaults to None
795799
(automatically seeded).
800+
:param type_dict: The mapping of integer to string types. If not specified, the
801+
default mapping is 1 -> 'H', 2 -> 'He', etc.
796802
"""
797803

798804
def __init__(
@@ -802,7 +808,8 @@ def __init__(
802808
*,
803809
gb_thickness: float = None,
804810
unit_cell: UnitCell = None,
805-
seed: int = None
811+
seed: int = None,
812+
type_dict: dict = {},
806813
) -> None:
807814
# initialize the random number generator
808815
if not seed:
@@ -817,21 +824,21 @@ def __init__(
817824
# not attempt to perform those in the case that only one GB is passed in.
818825
self.__one_parent = True
819826
self.__set_parents(system1, unit_cell=unit_cell,
820-
gb_thickness=gb_thickness)
827+
gb_thickness=gb_thickness, type_dict=type_dict)
821828
else:
822829
self.__one_parent = False
823830
self.__set_parents(system1, system2, unit_cell=unit_cell,
824-
gb_thickness=gb_thickness)
831+
gb_thickness=gb_thickness, type_dict=type_dict)
825832
self.__num_processes = mp.cpu_count() // 2 or 1
826833

827834
def __set_parents(
828-
self,
829-
system1: Union[GBMaker, str],
830-
system2: Union[GBMaker, str] = None,
831-
*,
832-
unit_cell=None,
833-
gb_thickness=None
834-
) -> None:
835+
self,
836+
system1: Union[GBMaker, str],
837+
system2: Union[GBMaker, str] = None,
838+
*,
839+
unit_cell: UnitCell = None,
840+
gb_thickness: float = None,
841+
type_dict: dict = {}) -> None:
835842
"""
836843
Method to assign the parent(s) that will create the child(ren).
837844
@@ -842,9 +849,12 @@ def __set_parents(
842849
:param gb_thickness: Keyword argument. The thickness of the GB region, optional,
843850
defaults to None. Note that if None is passed to the Parent class
844851
constructor, a value of 10 is assigned.
852+
:param type_dict: Keyword argument. Optional, defaults to an empty dict. The
853+
mapping from integer to elemental string. Default mapping is 1 -> 'H',
854+
2 -> 'He', etc.
845855
"""
846856
self.__parents[0] = Parent(
847-
system1, unit_cell=unit_cell, gb_thickness=gb_thickness)
857+
system1, unit_cell=unit_cell, gb_thickness=gb_thickness, type_dict=type_dict)
848858
if system2 is not None:
849859
# If there are 2 parents, with the first one being of type GBMaker, and
850860
# unit_cell has not been passed in, we assume that the unit cell from the
@@ -855,7 +865,7 @@ def __set_parents(
855865
if gb_thickness is None:
856866
gb_thickness = system1.gb_thickness
857867
self.__parents[1] = Parent(
858-
system2, unit_cell=unit_cell, gb_thickness=gb_thickness)
868+
system2, unit_cell=unit_cell, gb_thickness=gb_thickness, type_dict=type_dict)
859869

860870
@property
861871
def rng(self):
@@ -924,7 +934,7 @@ def remove_atoms(
924934
gb_fraction: float = None,
925935
num_to_remove: int = None,
926936
keep_ratio: bool = True,
927-
return_positions: bool = False
937+
return_positions: bool = False,
928938
) -> np.ndarray:
929939
"""
930940
Removes *gb_fraction* of atoms or *num_to_remove* atom(s) in the GB region. Uses
@@ -961,11 +971,9 @@ def remove_atoms(
961971
"0 < gb_fraction <= 0.25"
962972
)
963973

964-
if (num_to_remove is not None and
965-
(
966-
num_to_remove < 1 or num_to_remove > int(0.25 * len(gb_atoms))
967-
)
968-
):
974+
if num_to_remove is not None and (
975+
num_to_remove < 1 or num_to_remove > int(0.25 * len(gb_atoms))
976+
):
969977
raise GBManipulatorValueError(
970978
"Invalid num_to_remove value. Must be >= 1, and must be less than or "
971979
"equal to 25% of the total number of atoms in the GB region."
@@ -1004,12 +1012,12 @@ def remove_atoms(
10041012
parent.unit_cell.a0,
10051013
len(parent.unit_cell.unit_cell),
10061014
Delta,
1007-
Rmax
1015+
Rmax,
10081016
)
10091017
for idx, atom_idx in enumerate(gb_atom_indices)
10101018
]
10111019
order = np.zeros(len(args_list))
1012-
for (i, args) in enumerate(args_list):
1020+
for i, args in enumerate(args_list):
10131021
order[i] = _calculate_local_order(*args)
10141022

10151023
# We want the probabilities to be inversely proportional to the order parameter.
@@ -1047,8 +1055,10 @@ def remove_atoms(
10471055

10481056
central_num_to_remove = num_to_remove_dict[central_type]
10491057
selected_central_indices = self.__rng.choice(
1050-
central_indices, central_num_to_remove, replace=False,
1051-
p=central_probabilities
1058+
central_indices,
1059+
central_num_to_remove,
1060+
replace=False,
1061+
p=central_probabilities,
10521062
)
10531063

10541064
distances = {
@@ -1124,7 +1134,7 @@ def insert_atoms(
11241134
num_to_insert: int = None,
11251135
method: str = "delaunay",
11261136
keep_ratio: bool = True,
1127-
return_positions: bool = False
1137+
return_positions: bool = False,
11281138
) -> np.ndarray:
11291139
"""
11301140
Inserts **fraction** atoms in the GB at empty lattice sites. "Empty" sites are
@@ -1324,11 +1334,11 @@ def grid_approach(
13241334
)
13251335

13261336
if (num_to_insert is not None and
1327-
(
1337+
(
13281338
num_to_insert < 1 or
13291339
num_to_insert > int(0.25 * len(gb_atoms))
13301340
)
1331-
):
1341+
):
13321342
raise GBManipulatorValueError(
13331343
"Invalid num_to_insert value. Must be >= 1, and must be less than or "
13341344
"equal to 25% of the total number of atoms in the GB region.")
@@ -1376,15 +1386,19 @@ def grid_approach(
13761386
if keep_ratio and len(type_map) > 1:
13771387
central_num_to_insert = num_to_insert_dict[central_type]
13781388
selected_central_indices = self.__rng.choice(
1379-
list(range(len(possible_sites))), central_num_to_insert, replace=False,
1389+
list(range(len(possible_sites))),
1390+
central_num_to_insert,
1391+
replace=False,
13801392
p=probabilities
13811393
)
1382-
cutoff = (parent.unit_cell.nn_distance(2) +
1383-
parent.unit_cell.nn_distance(1)) / 2.0
1394+
cutoff = (
1395+
parent.unit_cell.nn_distance(2) + parent.unit_cell.nn_distance(1)
1396+
) / 2.0
13841397
possible_sites_neighbor_list = _create_neighbor_list(cutoff, possible_sites)
13851398

13861399
atoms_to_add = {
1387-
type_map[i]: [] if type_map[i] is not central_type else selected_central_indices for i in type_map.keys()}
1400+
type_map[i]: []
1401+
if type_map[i] is not central_type else selected_central_indices for i in type_map.keys()}
13881402
for atom_type, ratio in parent.unit_cell.ratio.items():
13891403
if atom_type == central_type:
13901404
continue
@@ -1437,7 +1451,7 @@ def displace_along_soft_modes(
14371451
mesh_size: int = 4,
14381452
num_q: int = 1,
14391453
num_children: int = 1,
1440-
subtract_displacement: bool = False
1454+
subtract_displacement: bool = False,
14411455
) -> np.ndarray:
14421456
"""
14431457
Displace atoms along soft phonon modes.

GBOpt/GBMinimizer.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def __init__(self, GB: GBMaker, gb_energy_func: Callable, choices: list, seed=ti
7171
self.manipulator.rng = self.local_random
7272
self.GBE_vals = []
7373

74-
def run_MC(self, E_accept: float = 1e-1, max_steps: int = 50, E_tol: float = 1e-4, max_rejections: int = 20, cooldown_rate: float = 1.0, unique_id: int = uuid.uuid4()) -> float:
74+
def run_MC(self, E_accept: float = 1e-1, max_steps: int = 50, E_tol: float = 1e-4, max_rejections: int = 20, cooldown_rate: float = 1.0, unique_id: int = uuid.uuid4(), **kwargs) -> float:
7575
# TODO: Add options for changing from linear to logarithmic cooldown
7676
"""
7777
Runs an MC loop on the grain boundary structure till the set convergence criteria are met.
@@ -82,6 +82,7 @@ def run_MC(self, E_accept: float = 1e-1, max_steps: int = 50, E_tol: float = 1e-
8282
:param max_rejections: Maximum number of consequtive rejections before the MC iterations are terminated.
8383
:param cooldown_rate: Factor ((0,1]) by which to reduce the 'temperature' of the MC simulation each iteration.
8484
:param unique_id: Unique unsigned integer to which to label all files generated by the MC run.
85+
:param **kwargs: Keyword arguments that are passed to gb_energy_func
8586
:return: Minimized energy value.
8687
"""
8788

@@ -93,7 +94,9 @@ def run_MC(self, E_accept: float = 1e-1, max_steps: int = 50, E_tol: float = 1e-
9394
self.manipulator,
9495
self.manipulator.parents[0].whole_system,
9596
"initial"+str(unique_id),
97+
**kwargs,
9698
)
99+
type_dict = {value: key for key, value in self.GB.unit_cell.type_map.items()}
97100
# Append grain boundary energy calculation to array
98101
self.GBE_vals.append(init_gbe)
99102

@@ -118,6 +121,7 @@ def run_MC(self, E_accept: float = 1e-1, max_steps: int = 50, E_tol: float = 1e-
118121
self.manipulator,
119122
new_system,
120123
str(unique_id),
124+
**kwargs,
121125
)
122126
self.GBE_vals.append(new_gbe)
123127

@@ -131,6 +135,7 @@ def run_MC(self, E_accept: float = 1e-1, max_steps: int = 50, E_tol: float = 1e-
131135
dump_file_name,
132136
unit_cell=self.GB.unit_cell,
133137
gb_thickness=self.GB.gb_thickness,
138+
type_dict=type_dict,
134139
)
135140
self.manipulator.rng = self.local_random
136141
prev_gbe = new_gbe

0 commit comments

Comments
 (0)