Theoretical foundations
This section presents the physics and numerics behind wetting_angle_kit, from the contact-angle definition to the extraction, wall detection, and fitting strategies of the analyzers.
1. The contact angle and the cap geometry
The contact angle \(\theta\) is the angle between the tangent to the liquid-vapor interface and the wall surface, measured through the liquid. For an idealised spherical-cap droplet of radius \(R\) whose centre sits at height \(z_c\) above the wall plane \(z = z_w\), simple geometry gives
Physically:
\(z_c < z_w\) (sphere centre below the wall) ⇒ \(\cos \theta > 0\) ⇒ \(\theta < 90^\circ\): hydrophilic.
\(z_c = z_w\): \(\theta = 90^\circ\) (hemisphere).
\(z_c > z_w\): \(\cos \theta < 0\) ⇒ \(\theta > 90^\circ\): hydrophobic.
The same identity governs cylindrical droplets, replacing the spherical cap by a circular cross-section in the plane perpendicular to the cylinder axis.
The job of the analysis pipeline is to estimate \(R\), \(z_c\), and \(z_w\) from atom positions, robustly enough that the recovered \(\theta\) is meaningful.
2. Geometric symmetry classes
Three geometries are supported via DropletGeometry:
"spherical"— full 3D droplet with no special axis."cylinder_y"— cylindrical droplet along the \(y\) axis."cylinder_x"— cylindrical droplet along the \(x\) axis.
|
|
The geometry choice cascades through every component:
spherical droplets are treated as fully three-dimensional objects;
cylindrical droplets exploit translational symmetry along the cylinder axis and can therefore be reduced to a two-dimensional fitting problem;
the sampling strategy, wall detection, and fitting procedure are automatically adapted to the selected geometry.
The cylindrical and spherical geometries share the same fitting framework and contact-angle definition. The only difference is that the cylindrical analysis is performed on cross-sections along the cylinder axis rather than on azimuthal slices.
3. The liquid–vapor interface in MD trajectories
There is no sharp surface in an MD frame: the density drops from \(\rho_{\rm liq}\) to \(\rho_{\rm vap}\) smoothly over a few Å, broadened by thermal motion. The package treats the liquid–vapor interface as the locus of half-bulk density. This interface is then used to fit a circle/sphere and recover the contact angle. The extraction of the interface is based on two choices:
The density field may be computed via a Gaussian KDE or a 3D top-hat binning.
The density may be sampled along rays from the droplet Center of Mass (COM) or on a fixed grid in space.
3.1. Estimating local density
We first need a local density estimate at each sample point.
Two estimators are available, swappable via DensityEstimator:
- Gaussian KDE
Each atom contributes a normalised 3D Gaussian of width \(\sigma\):
\[\rho_{\rm KDE}(\mathbf{r}) \;=\; \sum_i \frac{1}{(2\pi)^{3/2}\sigma^3}\, e^{-\|\mathbf{r} - \mathbf{r}_i\|^2 / 2\sigma^2}.\]Smooth and bias-controlled (the only knob is \(\sigma\)), which makes it the default choice. For efficiency, a per-atom cut-off at \(5\sigma\) is applied via a cKDTree.
- 3D top-hat
Atoms within \({\rm bin\_width}/2\) of the sample contribute uniformly:
\[\rho_{\rm bin}(\mathbf{r}) \;=\; \frac{N(\mathbf{r}, {\rm bin\_width}/2)}{V_{\rm bin}}.\]Fast and conceptually simple, but the hard cut-off introduces Poisson noise that can interfere with the tanh fit unless the bin width is matched to the smoothing length you’d otherwise pick.
Both estimators implement the same
DensityFieldProtocol, so the analysis pipeline can plug
either one into the same ray-fan or grid extraction.
3.2. Sampling the density field
Two strategies turn the density estimator into a clean point set on the interface:
- Ray fans
The
SpaceSampling.rays()factory emits a fan of rays from the droplet COM, samples the density along each ray, and recovers the interface position as the half-density point of a 1D tanh fit on that ray.In such samplings, the interface is recovered by fitting a one-dimensional hyperbolic-tangent profile to the density sampled along each ray:
\[\rho(\zeta) \;=\; h \;+\; d\,\tanh(\zeta_d - \zeta),\]where \(\zeta\) is the running coordinate along the ray and the three fitted parameters are the interface location \(\zeta_d\), the midpoint density \(h = (\rho_{\rm liq} + \rho_{\rm vap})/2\), and the half-amplitude \(d = (\rho_{\rm liq} - \rho_{\rm vap})/2\). The interface is \(\zeta = \zeta_d\), where \(\rho = h\).
The transition width is fixed — the tanh argument has unit slope, giving a transition scale of order 1 Å — rather than being a fitted parameter. Because the profile is antisymmetric about its midpoint, the recovered half-density crossing \(\zeta_d\) is largely insensitive to the exact width, so fixing the slope instead of fitting a thickness does not bias the interface location; only the amplitude/width interpretation would change, and the downstream geometry never uses it. (The coupled fit of §5 does treat the interface thicknesses \(t_1, t_2\) as free parameters, because there the full density field — not just the crossing — is modelled.)
This tanh profile is theoretically motivated by mean-field theory of liquid–vapor interfaces (van der Waals / Cahn–Hilliard square-gradient free energy) and is an excellent empirical fit to MD density profiles in the same regime.
Four ray-fan geometries are used depending on the
(surface_kind, droplet_geometry)pair:slicing + spherical: a 2D ray fan in each azimuthal plane through the droplet (planes spaced by
delta_azimuthal); within each plane, rays at polar angles spaced bydelta_polar.slicing + cylinder: a 2D ray fan in each
y-step plane (planes spaced bydelta_cylinder); same polar fan within each plane.whole + spherical: a full-sphere Fibonacci ray fan from the COM. Equal-area in \((\cos\theta, \phi)\) with the golden angle in \(\phi\); total ray count is
n_rays_sphere. Full-sphere coverage is important: downward rays from the COM hit the wall plane and produce shell points at \(z \approx z_w\), which is what makesWallDetector.min_plus_offset()work for the whole-fit.whole + cylinder: at each step along the cylinder axis (spaced by
delta_cylinder), a 2D ray fan is cast in the plane perpendicular to that axis; the full interface shell is the union of all per-step rings.
The choice of Fibonacci on the sphere is motivated by the fact that naive uniform \((\theta, \phi)\) gridding clusters rays near the poles, oversampling there and undersampling the equator. The Fibonacci spiral (uniform \(\cos\theta\), golden angle \(\phi\)) gives near-perfect equal-area coverage without clusters.
- Grid + iso-contour
The
SpaceSampling.grid()factory builds a fixed-cell grid in space and computes a density value at each cell, then recovers the interface as the iso-density contour at the half-bulk level viaskimage.measure.find_contours()in 2D (marching squares) orskimage.measure.marching_cubes()in 3D.In slicing mode, the grid sampling iterates per slice — azimuthal angles
γ ∈ [0°, 180°)for spherical droplets, axial steps alongyfor cylinder droplets — exactly like the rays variant. Each slice yields an(s, z)density field and one iso-contour; the downstreamSurfaceFitter.slicingaverages the per-slice angles and reports the inter-slice scatter, which is how the slicing method exposes droplet asymmetry.Two volume-normalisation notes:
grid+gaussianreturns 3D density per ų directly from the KDE evaluation; no extra volume normalisation needed.grid+binning’s slab-cut histogram divides byds × dz × dxso the recovered field is also in atoms/ų. The slab thickness equalsdx(the in-plane horizontal cell width), which keeps the bin’s cross-section in the(s, perpendicular)directions square.
4. Fitting the cap: algebraic Taubin fits
Given a clean point set on the interface, the surface fitter recovers the spherical-cap parameters \((z_c, R)\) (and \((x_c, y_c)\) in 3D) via an algebraic Taubin fit.
A circle/sphere is the zero set of \(g(\mathbf{r}) = A\,\|\mathbf{r}\|^2 + \mathbf{b}\cdot\mathbf{r} + c\) (a circle/sphere whenever \(A \neq 0\), with centre \(\mathbf{r}_c = -\mathbf{b}/(2A)\) and radius \(R = \sqrt{\|\mathbf{b}\|^2/(4A^2) - c/A}\)). The Taubin fit recovers the coefficients by minimising the algebraic residual normalised by its gradient,
The solution is closed-form: after centring the data it is the smallest right singular vector of a small design matrix (one SVD, no iteration and no initial guess). The 2D circle fit is the same construction with the \(y\) column dropped.
The gradient normalisation largely removes the bias that algebraic fits can exhibit on incomplete arcs. This is precisely the situation encountered for droplets, where only a portion of the underlying circle is observable. Since the recovered radius feeds directly into \(\cos\theta = (z_w - z_c)/R\), accurate fitting of partial arcs is essential. On synthetic datasets, Taubin fits agree with full orthogonal-distance fits to better than \(0.1^\circ\), while remaining a closed-form method that requires neither an initial guess nor numerical iteration.
The slicing fitter (SurfaceFitter.slicing()) runs one Taubin
circle fit per slice in the slice’s (x, z) plane, then
averages the per-slice angles. The whole fitter
(SurfaceFitter.whole()) runs one Taubin sphere fit
(spherical droplet) or one Taubin circle fit (cylindrical
droplet, exploiting translational symmetry along \(y\)) on the
entire shell.
5. Locating the wall plane
The contact angle is read from the cap–wall intersection, so the wall plane \(z_w\) has to be located explicitly:
WallDetector.min_plus_offset()— derive \(z_w\) from the interface itself, as \(z_w = \min(z_{\rm interface}) + \mathrm{offset}\). For slicing extractors the minimum across all slices’ interface points is taken on the contact line; for the full-sphere ray fan, downward rays from the COM reach the wall plane, so \(\min(z_{\rm shell})\) is again physically meaningful.WallDetector.from_atoms()— read wall-atom positions from the trajectory and place \(z_w\) at the mean of the top atomic layer (atoms withintop_layer_toleranceof the highest wall atom). Physically faithful when the simulation explicitly models the substrate.WallDetector.explicit()— caller supplies \(z_w\) directly. Useful when the wall position is known a priori from the simulation setup (e.g. a Lennard-Jones 9-3 wall at a known \(z\)-coordinate).
A consequence worth remembering: the recovered angle is sensitive to the wall position via the cap geometry \(\cos \theta = (z_w - z_c)/R\). A 1.5 Å shift in \(z_w\) on a 25 Å droplet at \(\theta \approx 95^\circ\) corresponds to roughly a 3° shift in the recovered angle. So either pick the wall detector that matches your trust budget, or report the angle for two choices to make the dependence visible.
6. Coupled fit
The CoupledFit2DAnalyzer and
CoupledFit3DAnalyzer skip the
extractor/wall/fitter decomposition and fit a multi-parameter
density model directly to a density field on a fixed grid.
The per-cell density is computed by the same pluggable
DensityEstimator strategy used elsewhere in the package:
either a top-hat histogram (DensityEstimator.binning(), the
default) or a 3D Gaussian KDE evaluated at the cell centres
(DensityEstimator.gaussian()). The binning variant is fast
and exact but intrinsically noisy at low per-cell atom counts; the
Gaussian variant smooths out Poisson noise at the cost of a small
constant overhead per batch. The choice of estimator does not
affect the model or the fit procedure — only the density values
fed into the NLLS solver.
6.1 The 2D model (7 parameters)
After projecting atoms to (xi, zi) via the droplet symmetry,
the analyzer computes a per-cell density and fits
with
The radial sigmoid \(g(r)\) describes the spherical-cap
interface; the vertical sigmoid \(h(z - z_0)\) cuts off the
density below the wall plane \(z_0\). The seven free
parameters \((\rho_1, \rho_2, R_{eq}, z_c, z_0, t_1, t_2)\) are
fit simultaneously by a bounded nonlinear least-squares
(scipy.optimize.curve_fit()).
The contact angle follows directly:
6.2 The 3D model (9 parameters)
The 3D extension computes a density on a full (xi, yi, zi)
Cartesian grid and fits
with two extra parameters \(\xi_c, \eta_c\) for the horizontal centre. Nine free parameters; same cap geometry for \(\theta\). Spherical droplets only (cylindrical droplets are rejected at construction because translational symmetry along the cylinder axis already collapses the 3D problem onto the 2D one).
6.3 Why a coupled fit?
The coupled fit shares information across the cap and the wall: the radial sigmoid is constrained by the apex curvature and the contact line simultaneously, and the vertical sigmoid pins the wall plane against the cap’s lower extent. Statistically more efficient than the decoupled pipeline when you can afford to pool many frames per batch; less informative per batch (single angle) and slower per batch (a 7-parameter NLLS rather than one closed-form Taubin solve per slice).
7. Periodic boundaries and droplet recentering
MD simulations are run with periodic boundary conditions; a droplet that drifts during the run can end up “split” across an \(x\) or \(y\) periodic edge by the time you analyse a given frame. A naive arithmetic mean of the atom positions would then place the “centre” inside the empty vapor region between the two halves of the droplet, ruining every downstream computation.
wetting_angle_kit uses a circular-mean recentering that handles this automatically. For each periodic direction \(u \in \{x, y\}\), atom positions \(u_i\) are first mapped to the unit circle:
The mean angle \(\bar\phi\) is the circular mean; the recentered atom positions are then
i.e. fold every atom into the box such that the droplet is centred on the box’s middle. Trajectories where the droplet drifts or wraps across a periodic edge are handled transparently; producing a pre-recentered trajectory at simulation time is optional.
The single precondition is that the simulation box must be large enough that the droplet does not interact with its periodic image — i.e. the droplet’s lateral diameter must be comfortably below the box length. If that condition is violated, the radial density profile is physically meaningless regardless of the centering strategy.
8. Frame batching
The TemporalAggregator groups trajectory frames into
batches before the full pipeline runs. Three regimes are useful:
batch_size=1One pipeline run per frame. Best for time-resolved studies; the per-frame
angle_std(per-slice scatter for slicing fits, bootstrap σ for whole fits) reports the within-frame uncertainty.batch_size=NPool \(N\) consecutive frames before the fit. Fewer batches, more atoms per fit → less noise per angle, but you lose time resolution within each batch.
batch_size=-1Pool every requested frame into a single batch — one angle for the whole trajectory. The default for the coupled-fit analyzers; useful for the slicing/whole pipeline too when you only want a representative angle.
The trade-off: the per-batch fit cost scales with the number of
atoms in the batch (roughly linearly for ray fans, sub-linearly
for grid binning), but the noise on the recovered angle scales
inversely with \(\sqrt{N}\) in regimes where shot noise
dominates. For a 4k-atom droplet on a typical room-temperature
trajectory, batch_size between 1 and 10 covers the useful
range.
9. Modular Workflow
The following diagram summarises the composable components of the analysis module and how they connect:
flowchart TD
%% ---------- role styles ----------
classDef decision fill:#fff3cd,stroke:#d39e00,color:#222;
classDef option fill:#e7f1ff,stroke:#4a86e8,color:#222;
classDef analyzer fill:#d4edda,stroke:#28a745,color:#222;
classDef shared fill:#f3e8ff,stroke:#8e44ad,color:#222;
start(["Analysis module"]):::analyzer
%% ===== Step 1: common to every analyzer =====
subgraph S1["Step 1 — common setup (all analyzers)"]
direction LR
geo{"Droplet geometry"}:::decision
geo --> spherical:::option
geo --> cylinder_x:::option
geo --> cylinder_y:::option
agg{"Temporal aggregation"}:::decision
agg --> b1["batch_size = 1\nper frame"]:::option
agg --> bn["batch_size = n\nn-frame blocks"]:::option
agg --> ball["batch_size = -1\nall frames pooled"]:::option
end
%% ===== Step 2: pick a method and configure it =====
subgraph S2["Step 2 — choose a method and configure it"]
method{"Which analyzer?"}:::decision
method -->|"separable steps,\nper-frame capable"| TA["TrajectoryAnalyzer"]:::analyzer
method -->|"coupled tanh fit,\n2D grid"| CF2["CoupledFit2DAnalyzer"]:::analyzer
method -->|"coupled tanh fit,\n3D grid (spherical only)"| CF3["CoupledFit3DAnalyzer"]:::analyzer
dens{"DensityEstimator"}:::shared
dens --> gaussian:::option
dens --> binning:::option
subgraph S2a["2a — TrajectoryAnalyzer components"]
ext["InterfaceExtractor\n= SpaceSampling + DensityEstimator"]:::shared
samp{"SpaceSampling"}:::decision
samp --> rays:::option
samp --> grid:::option
fit{"SurfaceFitter"}:::decision
fit --> slicing:::option
fit --> whole:::option
wall{"WallDetector"}:::decision
wall --> min_plus_offset:::option
wall --> explicit:::option
wall --> from_atoms:::option
ext --- samp
end
subgraph S2b["2b — coupled-fit components"]
grid_params["grid_params\n(or auto-derived)"]:::option
initial_params["initial_params\n(7-param tanh seed)"]:::option
end
TA --> S2a
CF2 --> S2b
CF3 --> S2b
ext -. uses .-> dens
CF2 -. uses .-> dens
CF3 -. uses .-> dens
fit -. "mode must match\nthe extractor" .- ext
end
start --> S1
S1 --> method