Molecular dynamics: depinning under applied force and torque

This notebook drives a rigid cluster with an applied external force \(F_x\) or torque \(\tau_\text{ext}\) and finds the depinning transition: the critical force \(F_c\) (or critical torque \(\tau_c\)) above which the cluster begins to slide.

The dynamics follow the overdamped Langevin (Euler–Maruyama) equations:

\[\eta_t \dot{x}_\text{cm} = F_x + F_x^\text{sub} + \xi_x\]
\[\eta_t \dot{y}_\text{cm} = F_y + F_y^\text{sub} + \xi_y\]
\[\eta_r \dot{\theta} = \tau_\text{ext} + \tau^\text{sub} + \xi_\theta\]

where \(\eta_t\) and \(\eta_r\) are the translational and rotational drag coefficients (computed from \(N\) and the moment of inertia), and the noise satisfies the fluctuation-dissipation theorem:

\[\langle \xi_i(t) \xi_j(t') \rangle = 2 k_B T \eta_i \, \delta_{ij} \delta(t - t')\]

sweep_md runs one MD trajectory per grid point (here: per \(F_x\) or \(\tau\) value) in parallel, then applies a post-processing function to extract a scalar metric (drift velocity or angular velocity). The depinning appears as a sharp onset in that metric.

[1]:
import numpy as np
from numpy import sqrt
import matplotlib.pyplot as plt
from time import time

from flake.substrate import substrate_from_params, get_ks
from flake.cluster import cluster_from_params, calc_cluster_langevin
from flake.dynamics import run_md
from flake.sweep import sweep_md, grid_sweep, drift_velocity, mean_velocity

System definition

Commensurate cluster on a sinusoidal triangular substrate (\(\rho = 1\)). You can setup the same geometry as the barrier notebook so the depinning force \(F_c\) is directly comparable to the static friction \(F_s\) computed from the MEP.

kBT = 1e-8 instead of 0 avoids saddle-point ambiguity in the Euler–Maruyama integrator. At \(\epsilon = 1\) this is thermally negligible (effectively \(T = 0\)).

[2]:
ks = get_ks(1, 3, 4./3., 0.)   # triangular substrate wave vectors

params = {
    'sub_basis':     [[0, 0]],
    'epsilon':       1,
    'well_shape':    'sin',
    'ks':            ks,
    'a1':            np.array([1.,  0.]),
    'a2':            np.array([0.5, -sqrt(3.)/2.]),
    'cl_basis':      [[0, 0]],
    'cluster_shape': 'circle',
    'N1': 15, 'N2': 15,
    'theta': 0.0, 'pos_cm': [0., 0.],
}

pen_func, en_func, en_inputs = substrate_from_params(params)
pos = cluster_from_params(params)
N   = pos.shape[0]

# Translational and rotational drag coefficients.
# eta_r / eta_t ~ <r^2> / 1 sets the ratio of rotational to translational
# relaxation times -- larger clusters relax rotationally more slowly.
eta = 1.0
eta_t, eta_r = calc_cluster_langevin(eta, pos)
pen = pen_func(pos + params['pos_cm'], params['pos_cm'], *en_inputs)[0]
print('N=%i  E=%.3g  eta_t=%.3g  eta_r=%.3g  eta_r/eta_t=%.3g'
      % (N, np.sum(pen), eta_t, eta_r, eta_r/eta_t))

fig, ax = plt.subplots(dpi=120, figsize=(4, 4))
sc = ax.scatter(pos[:, 0], pos[:, 1], c=pen, vmin=-params['epsilon'], vmax=0)
plt.colorbar(sc, ax=ax, label=r'$E_i$  [$\epsilon$]')
ax.set_xlabel('x  [a.u.]')
ax.set_ylabel('y  [a.u.]')
ax.set_title('Cluster reference frame, N=%d' % N)
ax.set_aspect('equal')
plt.tight_layout()
plt.show()
N=223  E=-223  eta_t=223  eta_r=6.86e+03  eta_r/eta_t=30.8
_images/4-Dynamics_3_1.png

Single trajectory: below and above depinning

Before running the full sweep it is instructive to watch individual trajectories at \(F_x\) just below and just above the depinning threshold. This gives physical intuition for what the sweep is measuring: - Pinned (\(F_x < F_c\)): the CM is stuck at fixed point. - Sliding (\(F_x > F_c\)): the CM drifts with a finite average velocity.

[3]:
base_kw = dict(
    eta=1.0, kBT=1e-8, dt=1e-3, n_steps=20000,
    Tau=0., print_every=10,
    pos_cm0=params['pos_cm'],
)

fig, axes = plt.subplots(1, 2, dpi=150, figsize=(8, 3))
for ax, Fx_test, label in [
    (axes[0], 200.,  'below Fc (pinned)'),
    (axes[1], 630.,  'above Fc (sliding)'),
]:
    traj = run_md(pos, en_func, en_inputs, Fx=Fx_test, **base_kw)
    t, xcm, ycm = traj['t'], traj['pos_cm'][:, 0], traj['pos_cm'][:, 1]
    ax.plot(t, xcm, label=r'$x_\mathrm{cm}$')
    ax.plot(t, ycm, label=r'$y_\mathrm{cm}$', ls='--')
    ax.set_xlabel('time  [a.u.]')
    ax.set_ylabel('position  [a.u.]')
    ax.set_title('Fx=%.0f  — %s' % (Fx_test, label))
    ax.legend(fontsize=8)
plt.tight_layout()
plt.show()
_images/4-Dynamics_5_0.png

## Force sweep: translational depinning

grid_sweep({'Fx': ...}) generates one parameter set per \(F_x\) value. drift_velocity() post-processes each trajectory to extract \(\langle v_x \rangle = (x_f - x_0)/(t_f - t_0)\) — a scalar that is \(\approx 0\) in the pinned state and grows linearly above depinning.

n_jobs=-1 uses all available CPUs; each trajectory is independent.

[4]:
t0 = time()

spec = grid_sweep({'Fx': np.linspace(0., 1000., 50)})

results = sweep_md(
    pos, en_func, en_inputs, spec,
    base_md_kwargs={
        'eta':         1.0,
        'kBT':         1e-8,
        'dt':          1e-3,
        'n_steps':     10000,
        'Tau':         0.,
        'print_every': 10,
        'pos_cm0':     params['pos_cm'],
    },
    post_fn=drift_velocity(),
    n_jobs=-1,
    save=False,
    verbose=False,
)

Fx_vals = np.array([r['params']['Fx'] for r in results])
vdrift  = np.array([r['result']       for r in results])

print('Done: %.1fmin' % ((time() - t0) / 60.))
Done: 0.2min

The depinning transition appears as a sharp onset in drift velocity.

  • Pinned (\(F_x < F_c\)): \(\langle v_x \rangle \approx 0\)

  • Sliding (\(F_x > F_c\)): \(\langle v_x \rangle\) grows linearly with \(F_x - F_c\)

The orange band brackets the transition region \([F_c^\text{lo},\, F_c^\text{hi}]\). The threshold 1e-1 is appropriate in this coarse example because kBT=1e-8, so thermal drift is negligible — any \(v_x > 10^{-2}\) is genuine sliding.

[5]:
fig, ax = plt.subplots(dpi=120, figsize=(5, 3.5))

pinned  = vdrift[:, 0] < 1e-1
sliding = ~pinned

ax.scatter(Fx_vals[pinned],  vdrift[pinned,  0], color='tab:blue',
           label='pinned', zorder=3)
ax.scatter(Fx_vals[sliding], vdrift[sliding, 0], color='tab:red',
           marker='^', label='sliding', zorder=3)

if sliding.any() and pinned.any():
    Fc_lo = Fx_vals[pinned].max()
    Fc_hi = Fx_vals[sliding].min()
    ax.axvspan(Fc_lo, Fc_hi, alpha=0.15, color='orange',
               label=r'$F_c \in [%.3f,\, %.3f]$' % (Fc_lo, Fc_hi))

ax.set_xlabel(r'$F_x$')
ax.set_ylabel(r'$\langle v_x \rangle$')
ax.legend(fontsize=8)
ax.set_title('Translational depinning  N=%i' % N)
plt.tight_layout()
plt.show()
_images/4-Dynamics_9_0.png

Torque sweep: rotational depinning

Same idea, but driving the cluster with an applied torque \(\tau_\text{ext}\) instead of \(F_x\). drift_omega extracts \(\langle \omega \rangle = (\theta_f - \theta_0)/(t_f - t_0)\), the angular drift velocity. A sharp onset in \(\langle \omega \rangle\) marks the rotational depinning threshold \(\tau_c\).

[6]:
def drift_omega():
    """Post-processing: return angular drift velocity dtheta/dt in deg/time."""
    def _post_fn(traj_dict, run_params):
        theta  = traj_dict['theta']
        t      = traj_dict['t']
        dt_tot = float(t[-1] - t[0])
        if dt_tot == 0.:
            return 0.
        return (theta[-1] - theta[0]) / dt_tot
    return _post_fn


t0 = time()

spec_tau = grid_sweep({'Tau': np.linspace(0., 6000., 50)})

results_tau = sweep_md(
    pos, en_func, en_inputs, spec_tau,
    base_md_kwargs={
        'eta':         1.0,
        'kBT':         1e-8,
        'dt':          1e-3,
        'n_steps':     50000,
        'print_every': 10,
    },
    post_fn=drift_omega(),
    n_jobs=-1,
    save=False,
    verbose=False,
)

Tau_vals = np.array([r['params']['Tau'] for r in results_tau])
odrift   = np.array([r['result']        for r in results_tau])

print('Done: %.1fmin' % ((time() - t0) / 60.))
Done: 0.2min
[7]:
fig, ax = plt.subplots(dpi=120, figsize=(5, 3.5))

pinned   = odrift < 1e-1
rotating = ~pinned

ax.scatter(Tau_vals[pinned],   odrift[pinned],   color='tab:blue',
           label='pinned', zorder=3)
ax.scatter(Tau_vals[rotating], odrift[rotating], color='tab:red',
           marker='^', label='rotating', zorder=3)

if rotating.any() and pinned.any():
    Tau_lo = Tau_vals[pinned].max()
    Tau_hi = Tau_vals[rotating].min()
    ax.axvspan(Tau_lo, Tau_hi, alpha=0.15, color='orange',
               label=r'$\tau_c \in [%.3f,\, %.3f]$' % (Tau_lo, Tau_hi))

ax.set_xlabel(r'$\tau$')
ax.set_ylabel(r'$\langle \omega \rangle$  [deg/time]')
ax.legend(fontsize=8)
ax.set_title('Rotational depinning  N=%i' % N)
plt.tight_layout()
plt.show()
_images/4-Dynamics_12_0.png