Skip to content

Commit c7f4014

Browse files
author
jpqiu
committed
Added support for customizing bulk regions and improved documentation
1 parent 805388c commit c7f4014

3 files changed

Lines changed: 102 additions & 12 deletions

File tree

docs/structures/README.md

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,52 @@ stc.find_surf_idx(element='Ti', tolerance=1, dsur='dw', check_cross_boundary=Tru
3030
[4, 9, 34, 39, 64, 69, 94, 99, 124, 129, 154, 159, 184, 189, 214, 219]
3131
# bottom surface
3232
[0, 5, 30, 35, 60, 65, 90, 95, 120, 125, 150, 155, 180, 185, 210, 215]
33-
```
33+
```
34+
35+
36+
## Generate an Interface Containing Ions or Organic Molecules in Distinct Regions
37+
38+
In many interfacial simulations, it is necessary to include ions or organic molecules—especially those that exhibit chemical adsorption. The generate_components_symmetry_box() method allows precise control over the spatial distribution of different species in the system.
39+
40+
For example, the image below shows an interface model constructed with ions and organics distributed in adsorption and bulk regions:
41+
![components](./figures/slab-components.png)
42+
43+
44+
```python
45+
#
46+
from ase.build import fcc111
47+
from ase.io import write
48+
from ectoolkits.structures.slab import Slab
49+
50+
# Build a 6×6×6 Cu(111) slab with orthogonal unit cell and 1.5 Å vacuum
51+
slab = fcc111('Cu', size=(6, 6, 6), a=3.57, vacuum=1.5, orthogonal=True)
52+
slab = Slab(slab)
53+
54+
# Define water box height
55+
water_box_len = 30
56+
57+
# Generate symmetric water box with:
58+
# - [Cl-] in the adsorption region (1 molecule)
59+
# - (CCOCCO), [K+], and [Cl-] in the bulk region (1, 3, and 3 molecules respectively)
60+
# Adsorption zone is 0.5, bulk region spans from 6 Å (bottom) to 10 Å (top)
61+
slab.generate_components_symmetry_box(
62+
water_box_len,
63+
ads_components_smiles=['[Cl-]'], ads_components_counts=[1],
64+
bulk_components_smiles=['CCOCCO', '[K+]', '[Cl-]'], bulk_components_counts=[1, 3, 3],
65+
ads_limit=0.5, bulk_dw=6, bulk_up=10
66+
)
67+
68+
# Identify top and bottom surface atom indices
69+
top_surface_idx = slab.find_surf_idx(element='Cu', tolerance=0.1, dsur='up')
70+
bottom_surface_idx = slab.find_surf_idx(element='Cu', tolerance=0.1, dsur='dw')
71+
72+
# Generate the final interface structure
73+
interface = slab.generate_interface(
74+
water_box_len=water_box_len,
75+
top_surface_idx=top_surface_idx,
76+
bottom_surface_idx=bottom_surface_idx
77+
)
78+
79+
# Write to XYZ file
80+
write('coord.xyz', interface)
81+
```
2.7 MB
Loading

ectoolkits/structures/slab.py

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -351,29 +351,40 @@ def generate_components_symmetry_box(self,
351351
ads_components_counts: list,
352352
bulk_components_smiles: list,
353353
bulk_components_counts: list,
354-
ads_limit = 0.5
354+
ads_limit = 0.5,
355+
bulk_dw= None,
356+
bulk_up= None
355357
):
356358
"""
357-
Generate water + ion/organic components box in a slab-aligned box
359+
Generate a slab-aligned box with symmetric distribution of water, adsorbed molecules, and bulk components.
360+
358361
Args:
359362
water_box_len (float): Height of the water box along z-direction.
360363
ads_components_smiles (list): SMILES list of adsorbed components.
361364
ads_components_counts (list): Corresponding counts of adsorbed components.
362365
bulk_components_smiles (list): SMILES list of bulk components.
363366
bulk_components_counts (list): Corresponding counts of bulk components.
364-
ads_limit (float): Maximum z-direction distance for adsorbates from the surface.
365-
For large molecules, this value may need to be increased. (default = 0.5 Å)
367+
ads_limit (float): Vertical range (in Å) above and below the surface for placing adsorbed species. Default = 0.5 Å.
368+
bulk_dw (float, optional): Lower boundary (Å) of the symmetric bulk region (below box center).
369+
bulk_up (float, optional): Upper boundary (Å) of the symmetric bulk region (below box center).
370+
Must satisfy: 0 < bulk_dw < bulk_up < water_box_len / 2.
371+
If set, creates symmetric regions for bulk placement.
372+
366373
367374
Returns:
368375
None. Generates a Packmol input file and runs Packmol to generate `watbox.xyz`.
369376
370377
Example:
371378
>>> from ectoolkits.structures.tools import generate_components_symmetry_box
372379
>>> generate_components_symmetry_box(
380+
... water_box_len = 20
373381
... ads_components_smiles=["[Cl-]"],
374382
... ads_components_counts=[2],
375383
... bulk_components_smiles=["[K+]", "[Cl-]"],
376-
... bulk_components_counts=[3, 3]
384+
... bulk_components_counts=[3, 3],
385+
... ads_limit=0.5,
386+
... bulk_dw = 3,
387+
... bulk_up = 6
377388
)
378389
379390
@@ -383,31 +394,40 @@ def generate_components_symmetry_box(self,
383394

384395
assert len(ads_components_smiles) == len(ads_components_counts), "Length mismatch in adsorbed components"
385396
assert len(bulk_components_smiles) == len(bulk_components_counts), "Length mismatch in bulk components"
397+
custom_bulk = False
398+
if bulk_dw is not None and bulk_up is not None:
399+
if bulk_dw > 0 and bulk_up < water_box_len / 2:
400+
custom_bulk = True
401+
else:
402+
raise ValueError(f"Invalid custom bulk region: bulk_dw={bulk_dw}, bulk_up={bulk_up}. "
403+
f"Ensure 0 < bulk_dw < bulk_up < water_box_len/2.")
404+
386405
cell = self.get_cell()
387406
cell_a = cell[0]
388407
cell_b = cell[1]
408+
409+
# === Water molecule estimation ===
389410
header = "-"
390411
logger.info(header * 50)
391412
logger.info("Now Generate Water Box")
392413
space_per_water = 9.86 ** 3 / 32
393-
init_wat_num = (np.linalg.norm(np.cross(cell_a, cell_b))
394-
* water_box_len) / space_per_water
395-
init_wat_num = int(init_wat_num)
414+
init_wat_num = int((np.linalg.norm(np.cross(cell_a, cell_b))
415+
* water_box_len) / space_per_water)
396416

397417
exclude_water = get_exclude_water_count(
398418
ads_components_smiles, ads_components_counts,
399419
bulk_components_smiles, bulk_components_counts
400420
)
401421

402422
wat_num = max(0, init_wat_num - 2 * exclude_water)
403-
wat_num = int(wat_num)
404423
logger.info(f"final number: {wat_num}")
405424
logger.info("Read Water Box Length: {0:03f} A".format(water_box_len))
406425
logger.info("Predict Water Number: {0}".format(wat_num))
407426

408427
n_vec_a, d1_a, d2_a, n_vec_b, d1_b, d2_b = get_plane_eq(cell_a, cell_b)
409428
logger.info("Calculate Plane Equation")
410429

430+
# === Create directory and input ===
411431
if os.path.exists('gen_water'):
412432
logger.info("found gen_water direcotry, now remove it")
413433
shutil.rmtree('gen_water')
@@ -422,24 +442,46 @@ def generate_components_symmetry_box(self,
422442
"filetype xyz",
423443
"output watbox.xyz\n"
424444
]
445+
# === Water ===
425446
water_xyz = os.path.join("gen_water", "water.xyz")
426447
smiles2xyz('O',water_xyz)
427448
lines.append(get_packmol_inp("water.xyz", wat_num, n_vec_a, n_vec_b, d1_a, d2_a, d1_b, d2_b, 0, water_box_len, radius=1))
428449

450+
# === Adsorbates ===
429451
for i, (smiles, count) in enumerate(zip(ads_components_smiles, ads_components_counts)):
430452
xyz_file = f"ads_component_{i}.xyz"
431453
xyz_path = os.path.join("gen_water", xyz_file)
432454
smiles2xyz(smiles, xyz_path)
455+
# bottom surface
433456
lines.append(get_packmol_inp(xyz_file, count, n_vec_a, n_vec_b, d1_a, d2_a, d1_b, d2_b, 0, ads_limit, radius=2))
457+
# top surface
434458
lines.append(get_packmol_inp(xyz_file, count, n_vec_a, n_vec_b, d1_a, d2_a, d1_b, d2_b, water_box_len - ads_limit, water_box_len, radius=2))
435459

460+
# === Bulk Components ===
436461
for i, (smiles, count) in enumerate(zip(bulk_components_smiles, bulk_components_counts)):
437462
xyz_file = f"bulk_component_{i}.xyz"
438463
xyz_path = os.path.join("gen_water", xyz_file)
439464
smiles2xyz(smiles, xyz_path)
440-
lines.append(get_packmol_inp(xyz_file, count, n_vec_a, n_vec_b, d1_a, d2_a, d1_b, d2_b, water_box_len / 4, water_box_len / 2, radius=2))
441-
lines.append(get_packmol_inp(xyz_file, count, n_vec_a, n_vec_b, d1_a, d2_a, d1_b, d2_b, water_box_len / 2, 3 * water_box_len / 4, radius=2))
465+
if custom_bulk:
466+
# lower layer
467+
lines.append(get_packmol_inp(xyz_file, count, n_vec_a, n_vec_b,
468+
d1_a, d2_a, d1_b, d2_b,
469+
bulk_dw, bulk_up, radius=2))
470+
# symmetric upper layer
471+
lines.append(get_packmol_inp(xyz_file, count, n_vec_a, n_vec_b,
472+
d1_a, d2_a, d1_b, d2_b,
473+
water_box_len - bulk_up, water_box_len - bulk_dw, radius=2))
474+
else:
475+
# default symmetric placement in central box
476+
lines.append(get_packmol_inp(xyz_file, count, n_vec_a, n_vec_b,
477+
d1_a, d2_a, d1_b, d2_b,
478+
water_box_len / 4, water_box_len / 2, radius=2))
479+
lines.append(get_packmol_inp(xyz_file, count, n_vec_a, n_vec_b,
480+
d1_a, d2_a, d1_b, d2_b,
481+
water_box_len / 2, 3 * water_box_len / 4, radius=2))
482+
442483
f.write("\n".join(lines))
484+
# === Run Packmol ===
443485
os.chdir("./gen_water")
444486
os.system("packmol < gen_wat_box.inp")
445487
os.chdir("../")

0 commit comments

Comments
 (0)