Phase separation & droplet physics
The pimms.lemonade.phase_separation and pimms.lemonade.surface_tension
modules quantify liquid-liquid phase separation of a PIMMS system: the coexistence
(binodal) densities, the amount of condensed material, cluster-size order
parameters, droplet shape and the interfacial tension.
import pimms.lemonade as lemonade
from pimms.lemonade import phase_separation as ps
from pimms.lemonade import surface_tension as st
traj = lemonade.load(xtc="traj.xtc", pdb="START.pdb", keyfile="KEYFILE.kf")
Densities throughout are occupied lattice-site fractions in [0, 1], so they
are directly comparable across box sizes.
Clusters and condensates
Clusters are connected groups of chains, found per frame by
Frame.clusters (largest first). The largest cluster
- the condensate - is frame.droplet. Each cluster carries its own geometry
(The object hierarchy): radius_of_gyration, asphericity, sphericity,
volume, surface_area, density, radial_density_profile() and its
composition.
Order parameters
Simple, per-frame measures of how phase separated the system is:
ps.condensed_fraction(traj) # (n_frames,) fraction of beads in the largest cluster
ps.largest_cluster_size(traj) # (n_frames,) beads in the largest cluster
ps.number_of_clusters(traj, min_beads=2)
ps.cluster_size_distribution(traj) # all cluster sizes pooled across frames
condensed_fraction is the basic order parameter: near zero when the system is
well mixed, approaching one when most material collects into a single condensate.
The binodal (coexistence densities)
The dense- and dilute-phase densities are read off a density profile fit to a hyperbolic tangent. Two geometries are supported.
Droplet (spherical). A radial profile about the condensate centre of mass - occupied fraction as a function of distance - falls from the dense core to the dilute background:
r, rho = ps.radial_density_profile(traj)
fit = ps.fit_radial_profile(r, rho)
fit.rho_dense, fit.rho_dilute # coexistence densities
fit.radius, fit.interface_width # droplet radius and interface width
The fit is \(\rho(r) = \tfrac12(\rho_d + \rho_v) - \tfrac12(\rho_d - \rho_v)\tanh((r-R)/w)\).
Slab. For a slab condensate that spans the periodic plane and is bounded along
one axis (the geometry of the slab_phase_separation demo), a 1D profile along
the long axis - with the slab re-centred each frame so it does not smear - is fit to
a two-interface tanh:
z, rho = ps.slab_density_profile(traj, axis=2) # axis defaults to the longest
fit = ps.fit_slab_profile(z, rho)
fit.rho_dense, fit.rho_dilute, fit.interface_width
One call for everything
analyze() runs the whole pipeline and
auto-detects the geometry (slab if one box axis is much longer than the others,
else spherical):
result = ps.analyze(traj)
result.geometry # 'sphere' or 'slab'
result.rho_dense, result.rho_dilute # binodal
result.condensed_fraction # time-averaged
result.binodal.interface_width
result.shape # {'radius_of_gyration', 'sphericity', ...}
result.is_phase_separated # heuristic: density gap AND most material condensed
result.profile # (coordinate, density) for plotting
Surface tension from undulations
Both surface-tension estimators use capillary-wave theory - the interface’s fluctuation spectrum. Because PIMMS uses \(\exp(-\Delta E/T)\) with \(k_B = 1\), the trajectory temperature is \(k_B T\), and the returned \(\gamma\) is in reduced units (interaction energy per lattice area).
st.surface_tension(traj) # auto-dispatch by geometry
st.slab_surface_tension(traj) # planar capillary waves (robust)
st.droplet_surface_tension(traj) # spherical-harmonic shape fluctuations
Slab - the two flat interfaces of a box-spanning condensate have a height field \(h(x,y)\) obeying \(\langle|h(q)|^2\rangle = k_BT/(\gamma A q^2)\); fitting the low-\(q\) spectrum gives \(\gamma\). This is the reliable method.
Droplet - the radius \(R(\theta,\phi)\) fluctuates in spherical-harmonic modes with \(\langle|u_{lm}|^2\rangle = k_BT/(\gamma R_0^2 (l-1)(l+2))\) for \(l \ge 2\). Best-effort: it needs a single, compact, reasonably large droplet sampled over many frames.
Each returns a SurfaceTension with the
estimate gamma, a per-mode spread gamma_std (an uncertainty proxy), the
number of modes used, and the raw spectrum for inspection:
result = st.slab_surface_tension(traj)
result.gamma, result.gamma_std
q, power = result.spectrum # inspect the capillary spectrum
Warning
Surface tension is intrinsically noisy for small lattice condensates (few
long-wavelength modes, rough interfaces). Always check gamma_std and plot the
spectrum; use a large interface (a big slab cross-section, or a single large
droplet) and many frames for a precise value. If the temperature is unknown
(loaded without a keyfile), pass temperature= explicitly.
Worked example
import pimms.lemonade as lemonade
from pimms.lemonade import phase_separation as ps
from pimms.lemonade import surface_tension as st
traj = lemonade.load(xtc="traj.xtc", pdb="START.pdb", keyfile="KEYFILE.kf")
result = ps.analyze(traj)
if result.is_phase_separated:
print(f"{result.geometry}: rho_dense = {result.rho_dense:.3f}, "
f"rho_dilute = {result.rho_dilute:.3f}, "
f"condensed fraction = {result.condensed_fraction:.2f}")
gamma = st.surface_tension(traj)
print(f"surface tension = {gamma.gamma:.2f} +/- {gamma.gamma_std:.2f} (kT units)")