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 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
@@ -0,0 +1,4 @@
1
+ from allomorph.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
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!')