Skip to content

A polycrystal of fcc and bcc grains

build_polycrystal fills every grain with the same crystal. For grains of two phases, build the polycrystal twice with the same seeds and orientations, once from each crystal: both then have the same grains, and each grain can be taken from either. This page builds a nanocomposite of copper (fcc) and tantalum (bcc) grains, removes the atoms that come too close where the phases meet, and checks each grain with common neighbor analysis.

The same grains twice

import numpy as np
import polars as pl
import mdapy as mp
from mdapy.build import build_crystal, build_polycrystal, random_rotations

box = mp.Box(100.0)
grains = 8
seeds = np.random.default_rng(7).random((grains, 3)) * 100.0
rotations = random_rotations(grains, seed=8)

copper = build_crystal("fcc", 3.615, elements="Cu")
tantalum = build_crystal("bcc", 3.303, elements="Ta")
fcc = build_polycrystal(copper, box, seeds, orientations=rotations)
bcc = build_polycrystal(tantalum, box, seeds, orientations=rotations)

Both have a grain column, numbered as the seeds. Take grains 0 to 3 from the copper polycrystal and the others from the tantalum one, and join the two tables:

in_copper = [0, 1, 2, 3]
table = pl.concat(
    [
        fcc.table.filter(pl.col("grain").is_in(in_copper)),
        bcc.table.filter(~pl.col("grain").is_in(in_copper)),
    ]
)
composite = mp.System(table=table, box=fcc.box)

Atoms too close

Inside one polycrystal, build_polycrystal already removed the atoms of neighboring grains that came too close. Where a copper grain meets a tantalum grain the two crystals were built separately, so atoms of the two can overlap. Of each pair closer than 0.7 times the nearest distance of copper, remove the later atom, as build_polycrystal does:

from mdapy.neighbor import NeighborList

close = NeighborList.build(composite, 0.7 * 3.615 / np.sqrt(2))
later = np.unique(np.maximum(close.i, close.j))
composite = composite.gather(np.setdiff1d(np.arange(len(composite)), later))
len(later), len(composite)  # 638 atoms removed, 66 672 left

The structure of each grain

Common neighbor analysis tells fcc from bcc. Attach the result as a column and count the structures in each grain with polars:

from mdapy.identify import CommonNeighborAnalysis

cna = CommonNeighborAnalysis().compute(composite)
labelled = composite.with_columns(cna)
counts = (
    labelled.table.group_by("grain", "element")
    .agg(
        atoms=pl.len(),
        fcc=(pl.col("cna_structure") == 1).mean(),
        bcc=(pl.col("cna_structure") == 3).mean(),
    )
    .sort("grain")
)
print(counts)

The copper grains come out about 70 % fcc and the tantalum grains about 70 % bcc; the rest are atoms at the boundaries, whose neighbors fit neither structure.

A picture

Color the atoms by structure and cut the box in half to see inside:

from mdapy.visualization import render

front = composite["positions"][:, 0] < 50.0
render(
    composite,
    "images/composite.png",
    atoms=front,
    color=cna,
    width=1200,
    height=900,
    legend=True,
)
render(
    composite,
    "images/composite-elements.png",
    atoms=front,
    width=1200,
    height=900,
    legend=True,
)
By structure: fcc green, bcc blue, others white By element
Grains colored by structure Grains colored by element

Before a simulation, relax the composite with a potential made for both elements (see Energy minimization): the boundaries between the phases rearrange, and how much depends strongly on the potential.