allomorph 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.
- allomorph/__init__.py +58 -0
- allomorph/__main__.py +4 -0
- allomorph/cli.py +85 -0
- allomorph/constants.py +267 -0
- allomorph/init_struct/__init__.py +45 -0
- allomorph/init_struct/gen_bnp_al.py +165 -0
- allomorph/init_struct/gen_bnp_cs.py +111 -0
- allomorph/init_struct/gen_mnp.py +285 -0
- allomorph/init_struct/gen_tnp_al.py +261 -0
- allomorph-0.1.0.dist-info/METADATA +168 -0
- allomorph-0.1.0.dist-info/RECORD +14 -0
- allomorph-0.1.0.dist-info/WHEEL +4 -0
- allomorph-0.1.0.dist-info/entry_points.txt +2 -0
- allomorph-0.1.0.dist-info/licenses/LICENSE +21 -0
allomorph/__init__.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Nanoparticle structure generation toolkit.
|
|
2
|
+
|
|
3
|
+
A package for generating monometallic to trimetallic nanoparticle structural
|
|
4
|
+
datasets for machine learning applications.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
from allomorph.constants import (
|
|
10
|
+
BNP_DIR,
|
|
11
|
+
BNP_DISTRIB_LIST,
|
|
12
|
+
DIAMETER_LIST,
|
|
13
|
+
ELE_DICT,
|
|
14
|
+
GOLDEN_RATIO,
|
|
15
|
+
LMP_DATA_DIR,
|
|
16
|
+
MNP_DIR,
|
|
17
|
+
RANDOM_DISTRIB_NO,
|
|
18
|
+
RATIO_LIST,
|
|
19
|
+
SHAPE_LIST,
|
|
20
|
+
TNP_DIR,
|
|
21
|
+
TNP_DISTRIB_LIST,
|
|
22
|
+
VACUUM_THICKNESS,
|
|
23
|
+
load_ele_dict_from_file,
|
|
24
|
+
parse_ele_comb,
|
|
25
|
+
validate_ele_dict,
|
|
26
|
+
)
|
|
27
|
+
from allomorph.init_struct.gen_bnp_al import gen_bnp, write_bnp
|
|
28
|
+
from allomorph.init_struct.gen_bnp_cs import gen_hard_core_shell, write_hard_core_shell
|
|
29
|
+
from allomorph.init_struct.gen_mnp import gen_mnp, write_mnp
|
|
30
|
+
from allomorph.init_struct.gen_tnp_al import gen_tnp, write_tnp
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"__version__",
|
|
34
|
+
"LMP_DATA_DIR",
|
|
35
|
+
"MNP_DIR",
|
|
36
|
+
"BNP_DIR",
|
|
37
|
+
"TNP_DIR",
|
|
38
|
+
"GOLDEN_RATIO",
|
|
39
|
+
"VACUUM_THICKNESS",
|
|
40
|
+
"RANDOM_DISTRIB_NO",
|
|
41
|
+
"ELE_DICT",
|
|
42
|
+
"DIAMETER_LIST",
|
|
43
|
+
"SHAPE_LIST",
|
|
44
|
+
"BNP_DISTRIB_LIST",
|
|
45
|
+
"TNP_DISTRIB_LIST",
|
|
46
|
+
"RATIO_LIST",
|
|
47
|
+
"validate_ele_dict",
|
|
48
|
+
"load_ele_dict_from_file",
|
|
49
|
+
"parse_ele_comb",
|
|
50
|
+
"gen_mnp",
|
|
51
|
+
"write_mnp",
|
|
52
|
+
"gen_bnp",
|
|
53
|
+
"write_bnp",
|
|
54
|
+
"gen_tnp",
|
|
55
|
+
"write_tnp",
|
|
56
|
+
"gen_hard_core_shell",
|
|
57
|
+
"write_hard_core_shell",
|
|
58
|
+
]
|
allomorph/__main__.py
ADDED
allomorph/cli.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Command-line interface for AlloMorph."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from allomorph.constants import load_config, update_constants
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main(argv=None):
|
|
10
|
+
"""Entry point for the AlloMorph CLI."""
|
|
11
|
+
parser = argparse.ArgumentParser(
|
|
12
|
+
prog="allomorph",
|
|
13
|
+
description="Toolkit for generating monometallic to trimetallic nanoparticle structural datasets.",
|
|
14
|
+
)
|
|
15
|
+
parser.add_argument(
|
|
16
|
+
"--config",
|
|
17
|
+
help="Path to a configuration file (JSON, YAML, or TOML) to override default constants.",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Subcommands
|
|
21
|
+
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
|
22
|
+
|
|
23
|
+
# init-struct subcommand (Main functionality)
|
|
24
|
+
init_parser = subparsers.add_parser(
|
|
25
|
+
"init-struct",
|
|
26
|
+
help="Generate initial nanoparticle structures.",
|
|
27
|
+
)
|
|
28
|
+
init_parser.add_argument(
|
|
29
|
+
"--stage",
|
|
30
|
+
choices=["mnp", "bnp", "tnp", "cs", "all"],
|
|
31
|
+
default="all",
|
|
32
|
+
help="Which structure generation stage to run (default: all)",
|
|
33
|
+
)
|
|
34
|
+
init_parser.add_argument(
|
|
35
|
+
"--replace",
|
|
36
|
+
action="store_true",
|
|
37
|
+
help="Overwrite existing files.",
|
|
38
|
+
)
|
|
39
|
+
init_parser.add_argument(
|
|
40
|
+
"--vis",
|
|
41
|
+
action="store_true",
|
|
42
|
+
help="Visualise generated structures (opens ASE GUI).",
|
|
43
|
+
)
|
|
44
|
+
init_parser.set_defaults(func=_init_struct_cmd)
|
|
45
|
+
|
|
46
|
+
args = parser.parse_args(argv)
|
|
47
|
+
|
|
48
|
+
if args.config:
|
|
49
|
+
config = load_config(args.config)
|
|
50
|
+
update_constants(config)
|
|
51
|
+
|
|
52
|
+
if args.vis:
|
|
53
|
+
print("Warning: --vis flag enabled. Visualization will run serially to prevent system hang.")
|
|
54
|
+
print("Many windows may be opened sequentially. Close one to see the next.")
|
|
55
|
+
|
|
56
|
+
if args.command is None:
|
|
57
|
+
parser.print_help()
|
|
58
|
+
sys.exit(1)
|
|
59
|
+
|
|
60
|
+
args.func(args)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _init_struct_cmd(args):
|
|
64
|
+
"""Run the initial structure generation command."""
|
|
65
|
+
from allomorph.init_struct.gen_bnp_al import main as gen_bnp_main
|
|
66
|
+
from allomorph.init_struct.gen_bnp_cs import write_hard_core_shell as gen_bnp_cs_main
|
|
67
|
+
from allomorph.init_struct.gen_mnp import main as gen_mnp_main
|
|
68
|
+
from allomorph.init_struct.gen_tnp_al import main as gen_tnp_main
|
|
69
|
+
|
|
70
|
+
if args.stage in ("mnp", "all"):
|
|
71
|
+
print("=== Generating monometallic nanoparticles (MNP) ===")
|
|
72
|
+
gen_mnp_main(replace=args.replace, vis=args.vis)
|
|
73
|
+
if args.stage in ("bnp", "all"):
|
|
74
|
+
print("=== Generating bimetallic nanoparticles (BNP) ===")
|
|
75
|
+
gen_bnp_main(replace=args.replace, vis=args.vis)
|
|
76
|
+
if args.stage in ("cs", "all"):
|
|
77
|
+
print("=== Generating hard core-shell nanoparticles (CS) ===")
|
|
78
|
+
gen_bnp_cs_main(replace=args.replace, vis=args.vis)
|
|
79
|
+
if args.stage in ("tnp", "all"):
|
|
80
|
+
print("=== Generating trimetallic nanoparticles (TNP) ===")
|
|
81
|
+
gen_tnp_main(replace=args.replace, vis=args.vis)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
main()
|
allomorph/constants.py
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Purpose: Store variables for generation of NPs using ASE
|
|
2
|
+
# Author: Jonathan Yik Chang Ting
|
|
3
|
+
# Date: 19/20/2020
|
|
4
|
+
|
|
5
|
+
from math import sqrt
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
LMP_DATA_DIR = "."
|
|
10
|
+
MNP_DIR = "MNP"
|
|
11
|
+
BNP_DIR = "BNP"
|
|
12
|
+
TNP_DIR = "TNP"
|
|
13
|
+
CS_DIR = "CS"
|
|
14
|
+
GOLDEN_RATIO = (1 + sqrt(5)) / 2
|
|
15
|
+
VACUUM_THICKNESS = 40.0
|
|
16
|
+
RANDOM_DISTRIB_NO = 3
|
|
17
|
+
|
|
18
|
+
# Elements of interest, their lattice parameters, metallic radii, and physical properties.
|
|
19
|
+
# - Pd, Pt, Au values were obtained from N. W. Ashcroft and N. D. Mermin,
|
|
20
|
+
# Solid State Physics (Holt, Rinehart, and Winston, New York, 1976.
|
|
21
|
+
# - The lattice constants are 3.859, 3.912, and 4.065 Angstroms, for the
|
|
22
|
+
# respective FCC metals at 300 K according to W. P. Davey,
|
|
23
|
+
# "Precision Measurements of the Lattice Constants of Twelve Common Metals,"
|
|
24
|
+
# Physical Review, vol. 25, (6), pp. 753-761, 1925.
|
|
25
|
+
# - Metallic radii taken from Greenwood, Norman N.; Earnshaw, Alan (1997).
|
|
26
|
+
# Chemistry of the Elements (2nd ed.).
|
|
27
|
+
# - Density (rho) in kg/m^3, molar mass (m) in kg/mol, and bulk cohesive
|
|
28
|
+
# energy (bulkE) in eV/atom are used by the feature-extraction pipeline.
|
|
29
|
+
ELE_DICT = {
|
|
30
|
+
"Pd": {"lc": {"FCC": 3.89}, "radius": 1.37, "mass": 106.42,
|
|
31
|
+
"rho": 12020, "m": 0.10642, "bulkE": 3.89},
|
|
32
|
+
"Pt": {"lc": {"FCC": 3.92}, "radius": 1.39, "mass": 195.08,
|
|
33
|
+
"rho": 21450, "m": 0.195084, "bulkE": 5.84},
|
|
34
|
+
"Au": {"lc": {"FCC": 4.09}, "radius": 1.44, "mass": 196.97,
|
|
35
|
+
"rho": 19320, "m": 0.196967, "bulkE": 3.81},
|
|
36
|
+
"Cu": {"lc": {"FCC": 3.61}, "radius": 1.28, "mass": 63.55,
|
|
37
|
+
"rho": 8960, "m": 0.063546, "bulkE": 3.49},
|
|
38
|
+
"Ni": {"lc": {"FCC": 3.52}, "radius": 1.25, "mass": 58.69,
|
|
39
|
+
"rho": 8908, "m": 0.058693, "bulkE": 4.44},
|
|
40
|
+
"Ag": {"lc": {"FCC": 4.09}, "radius": 1.44, "mass": 107.87,
|
|
41
|
+
"rho": 10490, "m": 0.107868, "bulkE": 2.95},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
DIAMETER_LIST = [10, 15, 20] # NP diameters of interest (Angstrom)
|
|
45
|
+
SHAPE_LIST = ["OT", "SP", "IC", "CU", "DH", "TH", "RD", "TO", "CO"] # Shapes of interest
|
|
46
|
+
BNP_DISTRIB_LIST = ["L10", "RAL", "RCS"] # BNP distributions of interest
|
|
47
|
+
TNP_DISTRIB_LIST = [
|
|
48
|
+
"L10R", "CS", "CL10S", "CRALS", "RRAL", "CSRAL", "CSL10", "CRSR", "LL10"
|
|
49
|
+
] # TNP distributions of interest
|
|
50
|
+
RATIO_LIST = [20, 40] # Ratios of interest
|
|
51
|
+
CUSTOM_SHAPES = {} # User-defined custom shapes
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def update_constants(config):
|
|
55
|
+
"""Update global constants from a dictionary.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
config: Dictionary containing constant names and their new values.
|
|
59
|
+
"""
|
|
60
|
+
global VACUUM_THICKNESS, RANDOM_DISTRIB_NO
|
|
61
|
+
if "VACUUM_THICKNESS" in config:
|
|
62
|
+
VACUUM_THICKNESS = config["VACUUM_THICKNESS"]
|
|
63
|
+
if "RANDOM_DISTRIB_NO" in config:
|
|
64
|
+
RANDOM_DISTRIB_NO = config["RANDOM_DISTRIB_NO"]
|
|
65
|
+
|
|
66
|
+
# For lists and dicts, we update in-place to preserve references in other modules
|
|
67
|
+
if "ELE_DICT" in config:
|
|
68
|
+
ELE_DICT.clear()
|
|
69
|
+
ELE_DICT.update(validate_ele_dict(config["ELE_DICT"]))
|
|
70
|
+
if "DIAMETER_LIST" in config:
|
|
71
|
+
DIAMETER_LIST[:] = config["DIAMETER_LIST"]
|
|
72
|
+
if "SHAPE_LIST" in config:
|
|
73
|
+
SHAPE_LIST[:] = config["SHAPE_LIST"]
|
|
74
|
+
if "BNP_DISTRIB_LIST" in config:
|
|
75
|
+
BNP_DISTRIB_LIST[:] = config["BNP_DISTRIB_LIST"]
|
|
76
|
+
if "TNP_DISTRIB_LIST" in config:
|
|
77
|
+
TNP_DISTRIB_LIST[:] = config["TNP_DISTRIB_LIST"]
|
|
78
|
+
if "RATIO_LIST" in config:
|
|
79
|
+
RATIO_LIST[:] = config["RATIO_LIST"]
|
|
80
|
+
if "CUSTOM_SHAPES" in config:
|
|
81
|
+
CUSTOM_SHAPES.clear()
|
|
82
|
+
CUSTOM_SHAPES.update(config["CUSTOM_SHAPES"])
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def load_config(path):
|
|
86
|
+
"""Load configuration from a JSON, YAML, or TOML file.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
path: Path to the configuration file.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The loaded configuration dictionary.
|
|
93
|
+
"""
|
|
94
|
+
import json
|
|
95
|
+
from pathlib import Path
|
|
96
|
+
|
|
97
|
+
suffix = Path(path).suffix.lower()
|
|
98
|
+
if suffix == ".json":
|
|
99
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
100
|
+
return json.load(f)
|
|
101
|
+
elif suffix in (".yaml", ".yml"):
|
|
102
|
+
try:
|
|
103
|
+
import yaml
|
|
104
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
105
|
+
return yaml.safe_load(f)
|
|
106
|
+
except ImportError as e:
|
|
107
|
+
raise ImportError("PyYAML is required to load YAML config files.") from e
|
|
108
|
+
elif suffix == ".toml":
|
|
109
|
+
try:
|
|
110
|
+
import tomllib # Python 3.11+
|
|
111
|
+
except ImportError:
|
|
112
|
+
try:
|
|
113
|
+
import tomli as tomllib
|
|
114
|
+
except ImportError as e:
|
|
115
|
+
raise ImportError(
|
|
116
|
+
"tomllib (Python 3.11+) or tomli is required to load TOML config files."
|
|
117
|
+
) from e
|
|
118
|
+
with open(path, "rb") as f:
|
|
119
|
+
return tomllib.load(f)
|
|
120
|
+
else:
|
|
121
|
+
raise ValueError(f"Unsupported config file format: {suffix}")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def validate_ele_dict(ele_dict):
|
|
125
|
+
"""Validate a user-supplied element dictionary.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
ele_dict: Dictionary mapping element symbols to property dicts.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
The validated dictionary.
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
ValueError: If required keys are missing or invalid.
|
|
135
|
+
"""
|
|
136
|
+
required_keys = {"lc", "radius", "mass"}
|
|
137
|
+
for element, props in ele_dict.items():
|
|
138
|
+
missing = required_keys - set(props.keys())
|
|
139
|
+
if missing:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"Element '{element}' is missing required keys: {missing}"
|
|
142
|
+
)
|
|
143
|
+
if "FCC" not in props.get("lc", {}):
|
|
144
|
+
raise ValueError(
|
|
145
|
+
f"Element '{element}' must have lattice constant 'lc' with an 'FCC' entry"
|
|
146
|
+
)
|
|
147
|
+
return ele_dict
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def load_ele_dict_from_file(path):
|
|
151
|
+
"""Load an element dictionary from a JSON file.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
path: Path to a JSON file containing the element dictionary.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Validated element dictionary.
|
|
158
|
+
"""
|
|
159
|
+
import json
|
|
160
|
+
|
|
161
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
162
|
+
ele_dict = json.load(f)
|
|
163
|
+
return validate_ele_dict(ele_dict)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def dist_1d(coord1, coord2, dim):
|
|
167
|
+
"""Compute distance between 2 points in one of their real space coordinates."""
|
|
168
|
+
return round(np.sqrt(np.sum((coord2[dim] - coord1[dim]) ** 2)), 3)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def dist_3d(coord1, coord2):
|
|
172
|
+
"""Compute real space distance between 2 points."""
|
|
173
|
+
return round(np.sqrt(np.sum((coord2 - coord1) ** 2)), 3)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def calc_rcs_prob(obj, shape):
|
|
177
|
+
"""Compute the core-shell probability for each atom in *obj*.
|
|
178
|
+
|
|
179
|
+
Returns a list where higher values correspond to surface-like positions.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
obj: ASE Atoms object.
|
|
183
|
+
shape: Nanoparticle shape string (e.g. 'IC', 'DH', 'OT').
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
List of probabilities, one per atom.
|
|
187
|
+
"""
|
|
188
|
+
prob_list = []
|
|
189
|
+
if shape == 'IC':
|
|
190
|
+
mass_center = obj.get_center_of_mass()
|
|
191
|
+
radius = (obj.cell[0][0] - VACUUM_THICKNESS) / 2
|
|
192
|
+
for atom in obj:
|
|
193
|
+
prob_list.append(dist_3d(mass_center, atom.position) / radius)
|
|
194
|
+
else:
|
|
195
|
+
x_slices = {round(atom.position[0], 3) for atom in obj}
|
|
196
|
+
y_slices = {round(atom.position[1], 3) for atom in obj}
|
|
197
|
+
z_slices = {round(atom.position[2], 3) for atom in obj}
|
|
198
|
+
|
|
199
|
+
z_thread = {(x, y): {'max': 0, 'min': 0, 'mid': []} for x in x_slices for y in y_slices}
|
|
200
|
+
x_thread = {(y, z): {'max': 0, 'min': 0, 'mid': []} for y in y_slices for z in z_slices}
|
|
201
|
+
y_thread = {(z, x): {'max': 0, 'min': 0, 'mid': []} for z in z_slices for x in x_slices}
|
|
202
|
+
for atom in obj:
|
|
203
|
+
x, y, z = round(atom.position[0], 3), round(atom.position[1], 3), round(atom.position[2], 3)
|
|
204
|
+
z_thread[(x, y)]['mid'].append(z)
|
|
205
|
+
x_thread[(y, z)]['mid'].append(x)
|
|
206
|
+
y_thread[(z, x)]['mid'].append(y)
|
|
207
|
+
for d in (z_thread, x_thread, y_thread):
|
|
208
|
+
empty = [k for k, v in d.items() if len(v['mid']) == 0]
|
|
209
|
+
for k in empty:
|
|
210
|
+
del d[k]
|
|
211
|
+
for k, v in d.items():
|
|
212
|
+
v['max'] = max(v['mid'])
|
|
213
|
+
v['min'] = min(v['mid'])
|
|
214
|
+
v['mid'] = (v['max'] + v['min']) / 2
|
|
215
|
+
|
|
216
|
+
for atom in obj:
|
|
217
|
+
x, y, z = round(atom.position[0], 3), round(atom.position[1], 3), round(atom.position[2], 3)
|
|
218
|
+
z_half = (z_thread[(x, y)]['max'] - z_thread[(x, y)]['min']) / 2
|
|
219
|
+
x_half = (x_thread[(y, z)]['max'] - x_thread[(y, z)]['min']) / 2
|
|
220
|
+
y_half = (y_thread[(z, x)]['max'] - y_thread[(z, x)]['min']) / 2
|
|
221
|
+
z_rel = abs(z - z_thread[(x, y)]['mid']) / z_half if round(z_half, 3) != 0.0 else abs(z - z_thread[(x, y)]['mid'])
|
|
222
|
+
x_rel = abs(x - x_thread[(y, z)]['mid']) / x_half if round(x_half, 3) != 0.0 else abs(x - x_thread[(y, z)]['mid'])
|
|
223
|
+
y_rel = abs(y - y_thread[(z, x)]['mid']) / y_half if round(y_half, 3) != 0.0 else abs(y - y_thread[(z, x)]['mid'])
|
|
224
|
+
if shape == 'DH':
|
|
225
|
+
prob = 1.0 if (round(z_rel, 3) == 1.0) or (round(z_half, 3) == 0.0) else z_rel
|
|
226
|
+
else:
|
|
227
|
+
if (round(z_rel, 3) == 1.0) or (round(x_rel, 3) == 1.0) or (round(y_rel, 3) == 1.0) or (round(z_half, 3) == 0.0) or (round(x_half, 3) == 0.0) or (round(y_half, 3) == 0.0):
|
|
228
|
+
prob = 1.0
|
|
229
|
+
else:
|
|
230
|
+
prob = (z_rel + x_rel + y_rel) / 3
|
|
231
|
+
prob_list.append(prob)
|
|
232
|
+
return prob_list
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def parse_ele_comb(ele_comb, ele_dict=None):
|
|
236
|
+
"""Parse a concatenated element combination string into individual symbols.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
ele_comb: String like 'AuPtPd' or 'CuAgAu'.
|
|
240
|
+
ele_dict: Optional element dictionary to use for matching. If None,
|
|
241
|
+
the built-in ELE_DICT is used.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
List of element symbols, e.g. ['Au', 'Pt', 'Pd'].
|
|
245
|
+
|
|
246
|
+
Raises:
|
|
247
|
+
ValueError: If the string cannot be parsed.
|
|
248
|
+
"""
|
|
249
|
+
if ele_dict is None:
|
|
250
|
+
ele_dict = ELE_DICT
|
|
251
|
+
symbols = sorted(ele_dict.keys(), key=len, reverse=True)
|
|
252
|
+
elements = []
|
|
253
|
+
i = 0
|
|
254
|
+
while i < len(ele_comb):
|
|
255
|
+
matched = False
|
|
256
|
+
for sym in symbols:
|
|
257
|
+
if ele_comb[i:].startswith(sym):
|
|
258
|
+
elements.append(sym)
|
|
259
|
+
i += len(sym)
|
|
260
|
+
matched = True
|
|
261
|
+
break
|
|
262
|
+
if not matched:
|
|
263
|
+
raise ValueError(
|
|
264
|
+
f"Could not parse element combination '{ele_comb}' at position {i}. "
|
|
265
|
+
f"Known symbols: {list(ele_dict.keys())}"
|
|
266
|
+
)
|
|
267
|
+
return elements
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Initial nanoparticle structure generation."""
|
|
2
|
+
|
|
3
|
+
from allomorph.constants import (
|
|
4
|
+
BNP_DIR,
|
|
5
|
+
BNP_DISTRIB_LIST,
|
|
6
|
+
DIAMETER_LIST,
|
|
7
|
+
ELE_DICT,
|
|
8
|
+
GOLDEN_RATIO,
|
|
9
|
+
LMP_DATA_DIR,
|
|
10
|
+
MNP_DIR,
|
|
11
|
+
RANDOM_DISTRIB_NO,
|
|
12
|
+
RATIO_LIST,
|
|
13
|
+
SHAPE_LIST,
|
|
14
|
+
TNP_DIR,
|
|
15
|
+
TNP_DISTRIB_LIST,
|
|
16
|
+
VACUUM_THICKNESS,
|
|
17
|
+
)
|
|
18
|
+
from allomorph.init_struct.gen_bnp_al import gen_bnp, write_bnp
|
|
19
|
+
from allomorph.init_struct.gen_bnp_cs import gen_hard_core_shell, write_hard_core_shell
|
|
20
|
+
from allomorph.init_struct.gen_mnp import gen_mnp, write_mnp
|
|
21
|
+
from allomorph.init_struct.gen_tnp_al import gen_tnp, write_tnp
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"LMP_DATA_DIR",
|
|
25
|
+
"MNP_DIR",
|
|
26
|
+
"BNP_DIR",
|
|
27
|
+
"TNP_DIR",
|
|
28
|
+
"GOLDEN_RATIO",
|
|
29
|
+
"VACUUM_THICKNESS",
|
|
30
|
+
"RANDOM_DISTRIB_NO",
|
|
31
|
+
"ELE_DICT",
|
|
32
|
+
"DIAMETER_LIST",
|
|
33
|
+
"SHAPE_LIST",
|
|
34
|
+
"BNP_DISTRIB_LIST",
|
|
35
|
+
"TNP_DISTRIB_LIST",
|
|
36
|
+
"RATIO_LIST",
|
|
37
|
+
"gen_mnp",
|
|
38
|
+
"write_mnp",
|
|
39
|
+
"gen_bnp",
|
|
40
|
+
"write_bnp",
|
|
41
|
+
"gen_tnp",
|
|
42
|
+
"write_tnp",
|
|
43
|
+
"gen_hard_core_shell",
|
|
44
|
+
"write_hard_core_shell",
|
|
45
|
+
]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Goal: Generate initial alloy BNP structures for MD simulations
|
|
2
|
+
# Author: Jonathan Yik Chang Ting
|
|
3
|
+
# Date: 22/10/2020
|
|
4
|
+
"""
|
|
5
|
+
Note:
|
|
6
|
+
- Abbreviations:
|
|
7
|
+
- RAL = randomly distributed alloy
|
|
8
|
+
- RCS = randomly distributed core-shell-like alloy
|
|
9
|
+
- (R)L10 = L1_0 intermetallic alloy (with/without random component)
|
|
10
|
+
- (R)L12 = L1_2 intermetallic alloy (with/without random component)
|
|
11
|
+
- To do:
|
|
12
|
+
- FCC is currently being hard-coded for lattice constant retrieval, might need to be flexible
|
|
13
|
+
- Perhaps could add a parameter to control core thickness
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from multiprocessing import Pool
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
from ase.io.lammpsdata import read_lammps_data, write_lammps_data
|
|
21
|
+
from ase.visualize import view
|
|
22
|
+
from numpy.random import RandomState
|
|
23
|
+
|
|
24
|
+
from allomorph.constants import (
|
|
25
|
+
BNP_DIR,
|
|
26
|
+
BNP_DISTRIB_LIST,
|
|
27
|
+
DIAMETER_LIST,
|
|
28
|
+
ELE_DICT,
|
|
29
|
+
LMP_DATA_DIR,
|
|
30
|
+
MNP_DIR,
|
|
31
|
+
RANDOM_DISTRIB_NO,
|
|
32
|
+
RATIO_LIST,
|
|
33
|
+
SHAPE_LIST,
|
|
34
|
+
VACUUM_THICKNESS,
|
|
35
|
+
calc_rcs_prob,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def rand_conv(obj, element1, element2, ele2Ratio, rseed, prob):
|
|
40
|
+
"""Randomly convert elements of atoms until specified ratio is reached"""
|
|
41
|
+
ele1Arr, ele2Arr = obj.symbols.search(element1), obj.symbols.search(element2)
|
|
42
|
+
ele2IdealNum = round(ele2Ratio / 100 * len(obj))
|
|
43
|
+
diff = len(ele2Arr) - ele2IdealNum
|
|
44
|
+
(convEleArr, targetEle) = (ele1Arr, element2) if diff < 0 else (ele2Arr, element1)
|
|
45
|
+
randGen = RandomState(rseed)
|
|
46
|
+
probArr = None
|
|
47
|
+
if len(prob) > 0:
|
|
48
|
+
weights = np.array(prob)[convEleArr]
|
|
49
|
+
total_weight = weights.sum()
|
|
50
|
+
if total_weight > 0:
|
|
51
|
+
probArr = weights / total_weight
|
|
52
|
+
idxArr = randGen.choice(a=convEleArr, size=abs(diff), replace=False, p=probArr)
|
|
53
|
+
for idx in idxArr:
|
|
54
|
+
obj[idx].symbol = targetEle
|
|
55
|
+
return obj
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def gen_bnp(obj, element1, element2, shape, ratio2, distrib, rseed, ele_dict=None):
|
|
59
|
+
"""Generates a BNP alloy structure based on specified distribution type."""
|
|
60
|
+
if ele_dict is None:
|
|
61
|
+
ele_dict = ELE_DICT
|
|
62
|
+
probList = []
|
|
63
|
+
if distrib == 'RAL':
|
|
64
|
+
# Handled by rand_conv if 'R' in distrib
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
elif distrib == 'RCS':
|
|
68
|
+
probList = calc_rcs_prob(obj, shape)
|
|
69
|
+
# Handled by rand_conv if 'R' in distrib
|
|
70
|
+
|
|
71
|
+
elif distrib in ['L10', 'L12', 'RL10', 'RL12']:
|
|
72
|
+
lc = ele_dict[element1]['lc']['FCC']
|
|
73
|
+
vacOffset = VACUUM_THICKNESS / 2
|
|
74
|
+
for (i, atom) in enumerate(obj):
|
|
75
|
+
yModulo = round((round(obj.positions[i][1], 3) - vacOffset) % lc, 3)
|
|
76
|
+
if (yModulo == 0.0) | (yModulo == lc):
|
|
77
|
+
atom.symbol = element2
|
|
78
|
+
if distrib == 'L12':
|
|
79
|
+
xModulo = round((round(obj.positions[i][0], 3) - vacOffset) % lc, 3)
|
|
80
|
+
if (xModulo == 0.0) | (xModulo == lc):
|
|
81
|
+
atom.symbol = element2
|
|
82
|
+
|
|
83
|
+
else:
|
|
84
|
+
raise Exception('Specified distribution type unrecognised!')
|
|
85
|
+
|
|
86
|
+
if 'R' in distrib:
|
|
87
|
+
obj = rand_conv(obj=obj, element1=element1, element2=element2, ele2Ratio=ratio2, rseed=rseed, prob=probList)
|
|
88
|
+
return obj
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def write_bnp(element1, element2, diameter, shape, ratio2, distrib, replace=False, vis=False, ele_dict=None):
|
|
92
|
+
"""Generates and writes a specific BNP alloy structure."""
|
|
93
|
+
if ele_dict is None:
|
|
94
|
+
ele_dict = ELE_DICT
|
|
95
|
+
output_base_dir = Path(LMP_DATA_DIR) / BNP_DIR
|
|
96
|
+
mnp_dir = Path(LMP_DATA_DIR) / MNP_DIR
|
|
97
|
+
|
|
98
|
+
file_name_mnp = f"{element1}{diameter}{shape}.lmp"
|
|
99
|
+
mnp_path = mnp_dir / file_name_mnp
|
|
100
|
+
|
|
101
|
+
if not mnp_path.exists():
|
|
102
|
+
# print(f" MNP file {mnp_path} not found, skipping...")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
mnp = read_lammps_data(str(mnp_path), atom_style='atomic', units='metal')
|
|
106
|
+
mnp.set_chemical_symbols(symbols=[element1] * len(mnp))
|
|
107
|
+
ratio1 = 100 - ratio2
|
|
108
|
+
|
|
109
|
+
distrib_dir = output_base_dir / distrib
|
|
110
|
+
distrib_dir.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
|
|
112
|
+
for rep in range(RANDOM_DISTRIB_NO):
|
|
113
|
+
if 'R' not in distrib:
|
|
114
|
+
r1, r2, rep_suffix = 50, 50, ''
|
|
115
|
+
else:
|
|
116
|
+
r1, r2, rep_suffix = ratio1, ratio2, str(rep)
|
|
117
|
+
|
|
118
|
+
file_name_bnp = f"{element1}{element2}{diameter}{shape}{r1}{r2}{distrib}{rep_suffix}.lmp"
|
|
119
|
+
output_path = distrib_dir / file_name_bnp
|
|
120
|
+
|
|
121
|
+
if not replace and output_path.exists():
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
bnp = gen_bnp(obj=mnp.copy(), element1=element1, element2=element2, shape=shape, ratio2=ratio2, distrib=distrib, rseed=rep if 'R' in distrib else 0, ele_dict=ele_dict)
|
|
125
|
+
write_lammps_data(str(output_path), atoms=bnp, units='metal', atom_style='atomic')
|
|
126
|
+
print(f" Generated {file_name_bnp}, formula: {bnp.get_chemical_formula()}")
|
|
127
|
+
|
|
128
|
+
if vis:
|
|
129
|
+
view(bnp, block=True)
|
|
130
|
+
if 'R' not in distrib:
|
|
131
|
+
break
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def main(replace=False, vis=False, ele_dict=None):
|
|
135
|
+
"""Main entry point for BNP generation."""
|
|
136
|
+
if ele_dict is None:
|
|
137
|
+
ele_dict = ELE_DICT
|
|
138
|
+
print('Generating BNP alloys:')
|
|
139
|
+
|
|
140
|
+
work_items = []
|
|
141
|
+
for diameter in DIAMETER_LIST:
|
|
142
|
+
for element in ele_dict:
|
|
143
|
+
for element2 in ele_dict:
|
|
144
|
+
if element == element2:
|
|
145
|
+
continue
|
|
146
|
+
for shape in SHAPE_LIST:
|
|
147
|
+
for ratio2 in RATIO_LIST:
|
|
148
|
+
for distrib in BNP_DISTRIB_LIST:
|
|
149
|
+
work_items.append((element, element2, diameter, shape, ratio2, distrib, replace, vis, ele_dict))
|
|
150
|
+
|
|
151
|
+
# Run in parallel unless visualization is requested
|
|
152
|
+
if vis:
|
|
153
|
+
print(" Visualization enabled: running serially...")
|
|
154
|
+
for item in work_items:
|
|
155
|
+
write_bnp(*item)
|
|
156
|
+
elif len(work_items) > 1:
|
|
157
|
+
with Pool() as p:
|
|
158
|
+
p.starmap(write_bnp, work_items)
|
|
159
|
+
elif work_items:
|
|
160
|
+
write_bnp(*work_items[0])
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == '__main__':
|
|
164
|
+
main(replace=False, vis=False)
|
|
165
|
+
print('ALL DONE!')
|