Deformation descriptors#
Once you have a reference configuration and a current (deformed) configuration of the same atoms, you can quantify what happened to the structure on a per-atom basis. pyscal3 ships four standard descriptors:
descriptor |
what it captures |
reference |
|---|---|---|
|
full 3×3 Green–Lagrange strain tensor |
Shimizu, Ogata, Li (2007) |
|
scalar shear invariant of the strain |
— |
|
non-affine residual after best-fit affine map |
Falk & Langer (1998) |
|
mean displacement difference vs reference |
Zimmerman et al. (2001) |
Use them to colour MD snapshots, locate slip bands, identify shear transformation zones in glasses, etc.
Important: both the reference and the current configurations must
have neighbours computed with the same cutoff. Atoms with fewer than
three matched neighbours return NaN.
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pyscal3 as pyscal
from pyscal3.structures import make_crystal
1. Uniaxial tension on FCC#
Stretch the lattice by 5 % along \(x\). The strain tensor should give \(E_{xx} \approx (1.05^2 - 1)/2 = 0.0513\), all other components zero.
ref = make_crystal('fcc', lattice_constant=4.05, repetitions=(4,4,4))
cur = ref.copy()
F = np.diag([1.05, 1.0, 1.0])
cur.set_positions(ref.get_positions() @ F.T)
cur.set_cell(np.array(ref.cell) @ F.T, scale_atoms=False)
for a in (ref, cur):
pyscal.find_neighbors(a, method='cutoff', cutoff=3.5)
E = pyscal.atomic_strain(cur, ref)
print('mean strain tensor (interior atom):')
print(E[len(cur)//2].round(4))
vm = pyscal.von_mises_strain(cur, ref)
print(f'\nvon Mises strain mean = {np.nanmean(vm):.4f}, std = {np.nanstd(vm):.2e}')
mean strain tensor (interior atom):
[[ 0.0513 -0. 0. ]
[-0. -0. 0. ]
[ 0. 0. 0. ]]
von Mises strain mean = 0.0513, std = 2.60e-16
Pure tension: a single non-zero diagonal component. The von Mises shear invariant is non-zero because uniaxial tension does produce shear in the deviatoric sense (volume-preserving comparison).
2. Simple shear#
Apply \(F_{xy} = \gamma\) with \(\gamma = 0.10\). Now we expect a strong shear contribution.
gamma = 0.10
ref = make_crystal('fcc', lattice_constant=4.05, repetitions=(4,4,4))
cur = ref.copy()
F = np.eye(3); F[0, 1] = gamma
cur.set_positions(ref.get_positions() @ F.T)
cur.set_cell(np.array(ref.cell) @ F.T, scale_atoms=False)
for a in (ref, cur):
pyscal.find_neighbors(a, method='cutoff', cutoff=3.5)
E = pyscal.atomic_strain(cur, ref)
vm = pyscal.von_mises_strain(cur, ref)
d2 = pyscal.d2min(cur, ref)
slp = pyscal.slip_vector(cur, ref)
print('strain tensor (interior atom):')
print(E[len(cur)//2].round(4))
print(f'von Mises strain : mean = {np.nanmean(vm):.4f}')
print(f'D\u00b2_min : mean = {np.nanmean(d2):.2e} (\u2248 0 for affine)')
print(f'slip vector : mean = {np.nanmean(slp, axis=0).round(4)}')
strain tensor (interior atom):
[[-0. 0.05 0. ]
[ 0.05 0.005 0. ]
[ 0. 0. 0. ]]
von Mises strain : mean = 0.0502
D²_min : mean = 4.32e-31 (≈ 0 for affine)
slip vector : mean = [-0. 0. 0.]
Two key sanity checks:
\(D^2_{\min}\) is essentially zero — the deformation is purely affine, so the residual after fitting an affine map is numerical noise.
The slip vector points along \(x\) on average, as expected for shear in the \(xy\)-plane.
3. Adding a non-affine perturbation#
Now superimpose random Gaussian displacements on top of the shear. This is what a real shear-transformation zone in a glass looks like. The \(D^2_{\min}\) map should light up where atoms moved away from the affine background.
rng = np.random.default_rng(0)
ref = make_crystal('fcc', lattice_constant=4.05, repetitions=(6,6,6))
cur = ref.copy()
F = np.eye(3); F[0, 1] = 0.05
cur.set_positions(ref.get_positions() @ F.T)
cur.set_cell(np.array(ref.cell) @ F.T, scale_atoms=False)
# Add localised non-affine kicks in a slab around z = z_mid
pos = cur.get_positions()
z_mid = pos[:, 2].mean()
in_band = np.abs(pos[:, 2] - z_mid) < 2.5
pos[in_band] += rng.normal(scale=0.25, size=(in_band.sum(), 3))
cur.set_positions(pos)
for a in (ref, cur):
pyscal.find_neighbors(a, method='cutoff', cutoff=3.5)
d2 = pyscal.d2min(cur, ref)
vm = pyscal.von_mises_strain(cur, ref)
fig = make_subplots(rows=1, cols=2,
subplot_titles=('D\u00b2_min', 'von Mises strain'))
fig.add_trace(go.Scatter(x=pos[:, 2], y=d2, mode='markers',
marker=dict(size=4, opacity=0.6, color=d2,
colorscale='Viridis'),
showlegend=False), row=1, col=1)
fig.add_trace(go.Scatter(x=pos[:, 2], y=vm, mode='markers',
marker=dict(size=4, opacity=0.6, color=vm,
colorscale='Plasma'),
showlegend=False), row=1, col=2)
fig.update_xaxes(title_text='z (\u00c5)', row=1, col=1)
fig.update_xaxes(title_text='z (\u00c5)', row=1, col=2)
fig.update_layout(template='simple_white', height=380,
title='Per-atom deformation metrics across the disordered band')
fig
Both metrics rise sharply inside the disordered slab. Note the complementary view: \(D^2_{\min}\) ignores any uniform shear (it sees only non-affine motion), while von Mises captures the local shear of the affine fit — they highlight overlapping but distinct populations.
4. 3D map of D\u00b2_min#
A scatter plot of the atoms in 3D, colour-coded by \(D^2_{\min}\), gives a direct visual answer to where the rearrangement happened.
fig = go.Figure(data=go.Scatter3d(
x=pos[:, 0], y=pos[:, 1], z=pos[:, 2],
mode='markers',
marker=dict(size=2.5, color=d2, colorscale='Viridis',
colorbar=dict(title='D\u00b2_min'), opacity=0.85)))
fig.update_layout(template='simple_white', height=520,
scene=dict(xaxis_title='x', yaxis_title='y', zaxis_title='z'),
title='Atoms coloured by non-affine displacement')
fig
5. Storage#
Each call writes its result back into atoms.arrays:
pyscal_strain— (N, 3, 3) Green–Lagrange tensorpyscal_von_mises— (N,) scalar shear invariantpyscal_d2min— (N,) non-affine residualpyscal_slip_vector— (N, 3) average displacement difference
# ensure all four arrays are present
pyscal.atomic_strain(cur, ref)
pyscal.von_mises_strain(cur, ref)
pyscal.d2min(cur, ref)
pyscal.slip_vector(cur, ref)
for k in ('pyscal_strain', 'pyscal_von_mises', 'pyscal_d2min', 'pyscal_slip_vector'):
print(f' {k:25s} shape={cur.arrays[k].shape}')
pyscal_strain shape=(864, 3, 3)
pyscal_von_mises shape=(864,)
pyscal_d2min shape=(864,)
pyscal_slip_vector shape=(864, 3)
Take-aways#
Affine deformations → use strain / von Mises.
Plastic / non-affine motion (shear bands, dislocations, glassy rearrangements) → use D²_min.
Use slip_vector to identify directions of slip in dislocation cores.
References#
M. L. Falk and J. S. Langer, Dynamics of viscoplastic deformation in amorphous solids, Phys. Rev. E 57, 7192 (1998).
F. Shimizu, S. Ogata and J. Li, Theory of shear banding in metallic glasses and molecular dynamics calculations, Mater. Trans. 48, 2923 (2007).
J. A. Zimmerman et al., Surface step effects on nanoindentation, Phys. Rev. Lett. 87, 165507 (2001).