API Reference

Complete reference for all modules, classes, and functions in wetting_angle_kit.

Parser Module

class wetting_angle_kit.parsers.AseParser(filepath: str)[source]

Bases: BaseParser

ASE trajectory parser for any ASE-readable file format.

box_length_max(frame_index: int) float[source]

Return the maximum lattice vector length for a frame.

Parameters:

frame_index (int) – Frame index.

Returns:

Max |a_i| over lattice vectors.

Return type:

float

box_size_x(frame_index: int) float[source]

Return the length of the first lattice vector for a frame.

box_size_y(frame_index: int) float[source]

Return the length of the second lattice vector for a frame.

frame_count() int[source]

Return the total number of frames available.

parse(frame_index: int, indices: ndarray | None = None) ndarray[source]

Return Cartesian coordinates for selected atoms in a frame.

Parameters:
  • frame_index (int) – Frame index.

  • indices (ndarray, optional) – Atom indices to select; if None all atoms are returned.

Returns:

Atom coordinates.

Return type:

ndarray, shape (M, 3)

parse_liquid_particles(liquid_particle_types: list[str], frame_index: int) ndarray[source]

Return positions of liquid atoms filtered by atomic symbol.

Parameters:
  • liquid_particle_types (sequence[str]) – Symbols identifying liquid atoms.

  • frame_index (int) – Frame index.

Returns:

Liquid atom positions.

Return type:

ndarray, shape (L, 3)

class wetting_angle_kit.parsers.AseWallParser(filepath: str, liquid_particle_types: list[str])[source]

Bases: BaseParser

Parser extracting wall particle coordinates (excluding liquid types).

Wall particles are everything not in liquid_particle_types. The indices argument of parse() is treated as 0-based positional indices into the wall-only positions for compatibility with the BaseParser contract.

box_length_max(frame_index: int) float[source]

Return the maximum lattice vector length for a frame.

Parameters:

frame_index (int) – Frame index.

Returns:

Max |a_i| over lattice vectors.

Return type:

float

box_size_x(frame_index: int) float[source]

Return the length of the first lattice vector for a frame.

box_size_y(frame_index: int) float[source]

Return the length of the second lattice vector for a frame.

find_highest_wall_part(*args: Any, **kwargs: Any) float[source]

Deprecated alias for find_highest_wall_particle.

find_highest_wall_particle(frame_index: int) float[source]

Return the maximum z-coordinate among wall particles for a frame.

Parameters:

frame_index (int) – Frame index.

Returns:

Maximum z-coordinate.

Return type:

float

frame_count() int[source]

Return the total number of frames in the trajectory.

parse(frame_index: int, indices: ndarray | None = None) ndarray[source]

Return wall atom positions for a frame.

Parameters:
  • frame_index (int) – Frame index.

  • indices (ndarray, optional) – Indices into the wall-only positions to further restrict the wall atoms; if None all wall atoms are returned.

Returns:

Wall atoms coordinates.

Return type:

ndarray, shape (M, 3)

class wetting_angle_kit.parsers.AseWaterFinder(filepath: str, oxygen_type: str = 'O', hydrogen_type: str = 'H', oh_cutoff: float = 1.2)[source]

Bases: object

Identify water oxygen atoms by counting hydrogen neighbors via ASE neighbor list.

get_water_oxygen_indices(frame_index: int) ndarray[source]

Return indices of oxygen atoms bonded to exactly two hydrogens.

Parameters:

frame_index (int) – Frame index.

Returns:

Oxygen atom indices belonging to water molecules.

Return type:

ndarray

get_water_oxygen_positions(frame_index: int) ndarray[source]

Return positions of water oxygen atoms for a frame.

Parameters:

frame_index (int) – Frame index.

Returns:

Oxygen atom positions; empty array if none detected.

Return type:

ndarray, shape (N, 3)

class wetting_angle_kit.parsers.BaseParser[source]

Bases: ABC

Abstract interface for trajectory parsers consumed by analyzers.

Subclasses must implement parse(), frame_count(), and the cell-geometry helpers box_size_x(), box_size_y(), and box_length_max(). The cell helpers are abstract because the analyzers rely on per-frame box information (PBC-aware droplet recentering, default sampling extent); a parser without it would silently degrade their accuracy.

abstractmethod box_length_max(frame_index: int) float[source]

Return the maximum lattice vector length for a frame.

Parameters:

frame_index (int) – Frame index.

Returns:

Max |a_i| over lattice vectors.

Return type:

float

abstractmethod box_size_x(frame_index: int) float[source]

Return the length of the first lattice vector for a frame.

abstractmethod box_size_y(frame_index: int) float[source]

Return the length of the second lattice vector for a frame.

abstractmethod frame_count() int[source]

Return the total number of frames in the trajectory.

abstractmethod parse(frame_index: int, indices: ndarray | None = None) ndarray[source]

Return Cartesian coordinates for selected atoms in a frame.

Parameters:
  • frame_index (int) – Frame index.

  • indices (ndarray, optional) –

    Atom indices to select; if None all atoms are returned. The meaning of indices differs by parser because the underlying file formats do not all preserve atom ordering across frames:

    • LammpsDumpParser (LAMMPS) — indices are LAMMPS particle identifiers. LAMMPS may reorder atoms between frames, so a persistent ID is needed to track the same atom.

    • XYZParser, AseParserindices are positional indices into the per-frame coordinate array. These formats keep atom ordering stable across frames during simulations.

Returns:

Atom coordinates.

Return type:

ndarray, shape (M, 3)

class wetting_angle_kit.parsers.LammpsDumpParser(filepath: str)[source]

Bases: BaseParser

LAMMPS dump trajectory parser backed by an OVITO pipeline.

box_length_max(frame_index: int) float[source]

Return the maximum lattice vector length for a frame.

Parameters:

frame_index (int) – Frame index.

Returns:

Max |a_i| over lattice vectors.

Return type:

float

box_size_x(frame_index: int) float[source]

Return the length of the first lattice vector for a frame.

box_size_y(frame_index: int) float[source]

Return the length of the second lattice vector for a frame.

frame_count() int[source]

Return the total number of frames in the trajectory.

parse(frame_index: int, indices: ndarray | None = None) ndarray[source]

Return Cartesian coordinates for selected atoms in a frame.

Parameters:
  • frame_index (int) – Frame index.

  • indices (ndarray, optional) – LAMMPS particle IDs to select; if None all atoms are returned.

Returns:

Atom coordinates.

Return type:

ndarray, shape (M, 3)

class wetting_angle_kit.parsers.LammpsDumpWallParser(filepath: str, liquid_particle_types: list[int])[source]

Bases: BaseParser

LAMMPS dump file parser for extracting wall particle coordinates.

Wall particles are everything not in liquid_particle_types; filtering is done inside the OVITO pipeline. The indices argument of parse() is therefore typically ignored, but it is accepted (as LAMMPS particle IDs, like LammpsDumpParser) to satisfy the BaseParser contract.

box_length_max(frame_index: int) float[source]

Return the maximum lattice vector length for a frame.

Parameters:

frame_index (int) – Frame index.

Returns:

Max |a_i| over lattice vectors.

Return type:

float

box_size_x(frame_index: int) float[source]

Return the length of the first lattice vector for a frame.

box_size_y(frame_index: int) float[source]

Return the length of the second lattice vector for a frame.

find_highest_wall_particle(frame_index: int) float[source]

Return the maximum z-coordinate among wall particles for a frame.

Parameters:

frame_index (int) – Frame index.

Returns:

Maximum z-coordinate.

Return type:

float

frame_count() int[source]

Return the total number of frames in the trajectory.

load_dump_ovito() Any[source]

Build and return the OVITO pipeline for wall-only extraction.

Returns Any because OVITO’s Python bindings ship without type stubs; the pipeline is opaque from the type checker’s perspective.

parse(frame_index: int, indices: ndarray | None = None) ndarray[source]

Return wall atom positions for a frame.

Parameters:
  • frame_index (int) – Frame index.

  • indices (ndarray, optional) – LAMMPS particle IDs to further restrict the wall atoms; if None all wall atoms are returned.

Returns:

Wall atom coordinates.

Return type:

ndarray, shape (M, 3)

class wetting_angle_kit.parsers.LammpsDumpWaterFinder(filepath: str, oxygen_type: int, hydrogen_type: int, oh_cutoff: float = 1.2)[source]

Bases: object

Identify water oxygen atoms in a LAMMPS trajectory via an OVITO pipeline.

get_water_oxygen_indices(frame_index: int) ndarray[source]

Return LAMMPS particle IDs of oxygen atoms bonded to exactly two hydrogens.

Parameters:

frame_index (int) – Frame index.

Returns:

Oxygen particle IDs belonging to water molecules.

Return type:

ndarray

class wetting_angle_kit.parsers.XYZParser(filepath: str)[source]

Bases: BaseParser

Extended XYZ trajectory parser.

box_length_max(frame_index: int) float[source]

Return the maximum lattice vector length for a frame.

Parameters:

frame_index (int) – Frame index.

Returns:

Max |a_i| over lattice vectors.

Return type:

float

box_size_x(frame_index: int) float[source]

Return the length of the first lattice vector for a frame.

box_size_y(frame_index: int) float[source]

Return the length of the second lattice vector for a frame.

frame_count() int[source]

Return the total number of frames in the trajectory.

load_xyz_file() list[dict[str, Any]][source]

Load all frames from the XYZ file into memory using numpy.

Returns:

Each entry has keys: symbols, positions, lattice_matrix.

Return type:

list[dict]

parse(frame_index: int, indices: ndarray | None = None) ndarray[source]

Return Cartesian coordinates for selected atoms in a frame.

Parameters:
  • frame_index (int) – Frame index.

  • indices (ndarray, optional) – Atom indices to select; if None all atoms are returned.

Returns:

Atom coordinates.

Return type:

ndarray, shape (M, 3)

parse_liquid_particles(liquid_particle_types: list[str], frame_index: int) ndarray[source]

Return positions of liquid atoms identified by their atomic symbol.

Parameters:
  • liquid_particle_types (sequence[str]) – Symbols identifying liquid atoms.

  • frame_index (int) – Frame index.

Returns:

Liquid atom positions.

Return type:

ndarray, shape (L, 3)

class wetting_angle_kit.parsers.XYZWallParser(filepath: str, liquid_particle_types: list[str])[source]

Bases: BaseParser

Parser extracting wall particle coordinates from an XYZ trajectory.

Wall particles are everything not in liquid_particle_types; the mask is applied at parse() time over the per-frame symbol array. The indices argument of parse() is treated as 0-based positional indices into the wall-only positions, mirroring AseWallParser.

box_length_max(frame_index: int) float[source]

Return the maximum lattice vector length for a frame.

box_size_x(frame_index: int) float[source]

Return the length of the first lattice vector for a frame.

box_size_y(frame_index: int) float[source]

Return the length of the second lattice vector for a frame.

find_highest_wall_particle(frame_index: int) float[source]

Return the maximum z-coordinate among wall particles for a frame.

frame_count() int[source]

Return the total number of frames in the trajectory.

parse(frame_index: int, indices: ndarray | None = None) ndarray[source]

Return wall atom positions for a frame.

Parameters:
  • frame_index (int) – Frame index.

  • indices (ndarray, optional) – 0-based indices into the wall-only positions to further restrict the result; if None all wall atoms are returned.

Returns:

Wall atom coordinates.

Return type:

ndarray, shape (M, 3)

class wetting_angle_kit.parsers.XYZWaterFinder(filepath: str, oxygen_type: str = 'O', hydrogen_type: str = 'H', oh_cutoff: float = 1.2)[source]

Bases: object

Helper for identifying water oxygen atoms in XYZ trajectories.

This is a standalone helper (not a BaseParser) because its parse signature filters by atomic symbol rather than frame index, which is incompatible with the parser ABC contract.

box_length_max(frame_index: int) float[source]

Return the maximum lattice vector length for a frame.

Parameters:

frame_index (int) – Frame index.

Returns:

Max |a_i| over lattice vectors.

Return type:

float

get_water_oxygen_indices(frame_index: int) ndarray[source]

Return indices of oxygen atoms bonded to exactly two hydrogens.

Parameters:

frame_index (int) – Frame index.

Returns:

Oxygen atom indices belonging to water molecules.

Return type:

ndarray

get_water_oxygen_positions(frame_index: int) ndarray[source]

Return positions of water oxygen atoms for a frame.

Parameters:

frame_index (int) – Frame index.

Returns:

Oxygen atom positions; empty array if none detected.

Return type:

ndarray, shape (N, 3)

load_xyz_file() list[dict[str, Any]][source]

Load frames including the lattice matrix for box-size queries.

parse(liquid_particle_types: list[str], frame_index: int) ndarray[source]

Return liquid particle coordinates filtering wall types.

Parameters:
  • liquid_particle_types (sequence[str]) – Symbols for liquid particles.

  • frame_index (int) – Frame index.

Returns:

Liquid atom positions.

Return type:

ndarray, shape (L, 3)

wetting_angle_kit.parsers.get_water_finder(filename: str, oxygen_type: Any, hydrogen_type: Any) Any[source]

Return the appropriate water oxygen finder for a given trajectory file.

Parameters:
  • filename (str) – Path to trajectory file; extension determines the finder class.

  • oxygen_type (Any) – Oxygen type identifier (symbol or integer depending on file format).

  • hydrogen_type (Any) – Hydrogen type identifier (symbol or integer depending on file format).

Returns:

Finder instance matching the file format.

Return type:

LammpsDumpWaterFinder | AseWaterFinder | XYZWaterFinder

Analysis

Top-level analyzers

The decomposed TrajectoryAnalyzer.

TrajectoryAnalyzer ties together the five strategy components that define a contact-angle analysis pipeline:

  • DropletGeometry — droplet symmetry / internal axis layout

  • TemporalAggregator — per-frame vs pooled-batch scheduling

  • InterfaceExtractor — atom → interface points

  • SurfaceFitter — interface points → contact angle

  • WallDetector — wall plane location

The class extends the shared _BatchedTrajectoryAnalyzer worker-pool scaffolding by implementing the four extension points documented there. The per-batch wiring lives in _process_batch_worker().

The coupled-fit analyzers (CoupledFit2DAnalyzer, CoupledFit3DAnalyzer) live in their own modules and share only the worker-pool scaffolding, not this strategy pipeline.

class wetting_angle_kit.analysis.trajectory.TrajectoryAnalyzer(parser: Any, atom_indices: ndarray | None = None, droplet_geometry: DropletGeometry | str = 'spherical', *, interface_extractor: InterfaceExtractor, surface_fitter: SurfaceFitter, wall_detector: WallDetector | None = None, temporal_aggregator: TemporalAggregator | None = None, precentered: bool = False, wall_atom_indices: ndarray | None = None)[source]

Bases: _BatchedTrajectoryAnalyzer

Decomposed contact-angle analyzer: extractor → wall → fitter.

Parameters:
  • parser (BaseParser) – Trajectory parser instance. Only parser.filepath and parser.frame_count() are read in the parent process; each worker rebuilds its own parser from filepath.

  • atom_indices (ndarray, optional) – Indices of the liquid atoms; passed through to the parser’s per-frame parse call. Empty by default.

  • droplet_geometry (DropletGeometry or str, default "spherical") – Either a DropletGeometry instance or the bare name string. Drives the internal axis convention and the per-slice layout used by the extractor.

  • interface_extractor (InterfaceExtractor) – Composes a SpaceSampling (built via SpaceSampling.rays() or SpaceSampling.grid()) with a DensityEstimator (built via DensityEstimator.gaussian() or DensityEstimator.binning()).

  • surface_fitter (SurfaceFitter) – Built via SurfaceFitter.slicing() or SurfaceFitter.whole(). Its kind must match the extractor’s natural output, which is enforced via InterfaceExtractor.validate_compatibility() at construction.

  • wall_detector (WallDetector, optional) – Built via WallDetector.min_plus_offset() / WallDetector.explicit() / WallDetector.from_atoms(). Defaults to WallDetector.min_plus_offset(offset=2.0).

  • temporal_aggregator (TemporalAggregator, optional) – Defaults to per-frame analysis (batch_size=1).

  • precentered (bool, default False) – Skip per-frame circular-mean PBC recentering. Setting this on a trajectory that does NOT satisfy the precondition will produce wrong results.

  • wall_atom_indices (ndarray, optional) – Required when wall_detector is a WallDetector.from_atoms() instance. The analyzer gathers and pools these coordinates per batch and supplies them to the detector via WallContext.wall_coords.

Coupled-fit contact-angle analyzers.

Two top-level analyzers that solve interface extraction, wall detection, and surface fitting together via a hyperbolic-tangent density model:

  • CoupledFit2DAnalyzer — seven-parameter fit on a 2D (xi, zi) density grid (radial symmetry assumption).

  • CoupledFit3DAnalyzer — nine-parameter fit on a full 3D (xi, yi, zi) density grid (no symmetry assumption; spherical droplets only — cylinder droplets are rejected at construction).

Both analyzers accept a DensityEstimator strategy that controls how the per-cell density is computed from pooled atom positions: DensityEstimator.binning() (top-hat histogram, the default) or DensityEstimator.gaussian() (3D Gaussian KDE on the cell centres). Switching to the Gaussian variant trades a small constant cost per fit for a smooth density field with no per-cell Poisson noise.

Use these analyzers when you have many frames per batch and want a single robust estimate; use TrajectoryAnalyzer with separable strategies for per-frame time resolution.

class wetting_angle_kit.analysis.coupled_fit.CoupledFit2DAnalyzer(parser: Any, atom_indices: ndarray | None = None, droplet_geometry: DropletGeometry | str = 'spherical', *, grid_params: dict[str, Any] | None = None, density_estimator: DensityEstimator | None = None, initial_params: list[float] | None = None, temporal_aggregator: TemporalAggregator | None = None, precentered: bool = False)[source]

Bases: _CoupledFitAnalyzer

Coupled contact-angle fit on a 2D density grid.

Parameters:
  • parser (BaseParser) – Trajectory parser. Only parser.filepath and parser.frame_count() are read in the parent process; each worker rebuilds its own parser.

  • atom_indices (ndarray, optional) – Indices of the liquid atoms.

  • droplet_geometry (DropletGeometry or str, default "spherical") – Either an instance or the bare name string. Determines the per-frame projection onto the (xi, zi) plane: spherical droplets use the in-plane radial coordinate xi = sqrt(x^2 + y^2); cylindrical droplets use the coordinate perpendicular to the cylinder axis.

  • grid_params (dict, optional) – 2D grid spec with keys "xi_0", "xi_f", "dx", "zi_0", "zi_f", "dz". The range bounds are honoured exactly; the effective cell width is rounded to fit and may differ slightly from the requested dx / dz. If None, an atom-derived default is used: xi/zi span half the largest in-plane box dimension, with dx / dz = 0.5 Å (half the model’s default interface thickness t1). A warning is emitted when the default is used.

  • density_estimator (DensityEstimator, optional) – How the per-cell density is computed from the pooled atom positions. Built via DensityEstimator.binning() (the default, top-hat histogram with geometry-aware dV normalisation) or DensityEstimator.gaussian() (3D Gaussian KDE evaluated at the cell centres; the same kernel SpaceSampling.rays() / SpaceSampling.grid() with DensityEstimator.gaussian() use). Switching to the Gaussian variant smooths out per-cell Poisson noise — useful on per-frame / small-batch analyses where the histogram density is degenerate.

  • initial_params (list[float], optional) – Initial guess for the seven tanh-model parameters [rho1, rho2, R_eq, zi_c, zi_0, t1, t2]. Defaults to the values tuned for room-temperature water in the existing _HyperbolicTangentModel2D.

  • temporal_aggregator (TemporalAggregator, optional) – Defaults to a single fully pooled batch (batch_size=-1) — the coupled fit benefits from as much statistics as possible. Set batch_size=N to compute independent angles for each N-frame block.

  • precentered (bool, default False) – Skip per-frame circular-mean PBC recentering. Setting this on a trajectory that does NOT satisfy the precondition will produce wrong results.

class wetting_angle_kit.analysis.coupled_fit.CoupledFit3DAnalyzer(parser: Any, atom_indices: ndarray | None = None, droplet_geometry: DropletGeometry | str = 'spherical', *, grid_params: dict[str, Any] | None = None, density_estimator: DensityEstimator | None = None, initial_params: list[float] | None = None, temporal_aggregator: TemporalAggregator | None = None, precentered: bool = False)[source]

Bases: _CoupledFitAnalyzer

Coupled contact-angle fit on a 3D binned density grid.

Parameters:
  • parser (BaseParser) – Trajectory parser. Only parser.filepath and parser.frame_count() are read in the parent process; each worker rebuilds its own parser.

  • atom_indices (ndarray, optional) – Indices of the liquid atoms.

  • droplet_geometry (DropletGeometry or str, default "spherical") – Must be spherical. Cylindrical droplets are rejected at construction because their translational symmetry already collapses the 3D problem onto the 2D one solved by CoupledFit2DAnalyzer.

  • grid_params (dict, optional) – 3D grid spec with keys "xi_0", "xi_f", "dx", "yi_0", "yi_f", "dy", "zi_0", "zi_f", "dz". The range bounds are honoured exactly; the effective cell width is rounded to fit. If None, an atom-derived default is used (lateral half-box for all axes, dx / dy / dz = 1 Å to keep the 9-parameter NLLS tractable). xi/yi are in the droplet-centred frame (atoms are recentred on the per-frame COM before binning); zi is in the lab frame so the wall position retains physical meaning. If None, a heuristic default is used.

  • initial_params (list[float], optional) – Initial guess for the nine tanh-model parameters [rho1, rho2, R_eq, xi_c, yi_c, zi_c, zi_0, t1, t2].

  • temporal_aggregator (TemporalAggregator, optional) – Defaults to a single fully pooled batch (batch_size=-1). The 3D density needs more frames than the 2D one for comparable per-cell noise.

  • precentered (bool, default False) – Skip per-frame circular-mean PBC recentering.

class wetting_angle_kit.analysis.coupled_fit.DensityEstimator[source]

Bases: ABC

Strategy interface for density estimation.

Concrete instances come from one of the classmethod factories binning() or gaussian(); the abstract methods are dispatched by the analyzer / extractor that consumes them.

classmethod binning(*, bin_width: float | None = None) DensityEstimator[source]

Top-hat histogram density estimator.

Parameters:

bin_width (float, optional) – Side length (Å) of the 3D top-hat kernel used by build_field() for pointwise evaluation (SpaceSampling.rays()). Ignored by evaluate_on_slice(), evaluate_on_3d_grid(), evaluate_2d(), and evaluate_3d() — those consumers derive their cell sizes from the grid spec they’re given. Required only when the estimator is consumed pointwise.

abstractmethod build_field(atoms: ndarray) DensityFieldProtocol[source]

Pointwise 3D density evaluator on the given atom set.

Returns an object exposing evaluate(positions) for arbitrary (N, 3) query points. Used by SpaceSampling.rays() to sample density along each ray.

The binning estimator requires bin_width to have been set on the factory call; calling build_field() without one raises ValueError.

abstractmethod evaluate_2d(*, atoms_pooled: ndarray, n_frames: int, droplet_geometry: DropletGeometry, xi_edges: ndarray, zi_edges: ndarray, box_dimension: float | None) ndarray[source]

Coupled-fit 2D: radial-projected (xi, zi) density.

Returns a (n_xi, n_zi) array in atoms/ų, averaged across the n_frames pooled into the batch. For spherical, atoms fold onto xi = hypot(x, y) with annular dV; for cylinder, atoms fold onto xi = |x| with cylinder-length dV.

abstractmethod evaluate_3d(*, atoms_pooled: ndarray, n_frames: int, droplet_geometry: DropletGeometry, xi_edges: ndarray, yi_edges: ndarray, zi_edges: ndarray) ndarray[source]

Coupled-fit 3D: Cartesian (xi, yi, zi) density.

Returns a (n_xi, n_yi, n_zi) array in atoms/ų, averaged across the n_frames pooled into the batch.

Only spherical is currently exercised — the 3D coupled-fit analyzer rejects cylinder droplets at construction.

abstractmethod evaluate_on_3d_grid(atoms: ndarray, x_centers: ndarray, y_centers: ndarray, z_centers: ndarray, *, x_offset: float, y_offset: float) ndarray[source]

3D density on the cell centres of a Cartesian grid.

Returns a (len(x_centers), len(y_centers), len(z_centers)) array. The grid is laterally droplet-centred (x_offset, y_offset shift the cell coordinates back to the lab frame for evaluation against the lab-frame atoms).

abstractmethod evaluate_on_slice(atoms: ndarray, slice_center: ndarray, in_plane_axis: ndarray, s_centers: ndarray, z_centers: ndarray, slab_thickness: float) ndarray[source]

2D density on the cell centres of a slice plane.

Returns a (len(s_centers), len(z_centers)) array. The slice plane is defined by slice_center (a 3D point on the plane) and in_plane_axis (a horizontal unit vector defining the radial coordinate s).

For the Gaussian estimator, the KDE is evaluated at each cell-centre 3D point on the plane. For the binning estimator, atoms inside the slab |perp| slab_thickness / 2 are histogrammed in (s, z); each cell’s density is counts / (ds · dz · slab_thickness).

classmethod gaussian(*, density_sigma: float = 3.0, cutoff_sigma: float = 5.0) DensityEstimator[source]

3D Gaussian KDE density estimator.

Parameters:
  • density_sigma (float, default 3.0) – Gaussian kernel width (Å).

  • cutoff_sigma (float, default 5.0) – Per-atom kernel truncation in units of density_sigma. Larger values are slower but more accurate in the kernel’s tails.

kind: ClassVar[str]

kind tag (used in tqdm labels).

Strategy components

The interface-finding subsystem.

The submodule owns everything related to recovering the liquid–vapor interface from atom positions. An InterfaceExtractor composes two orthogonal strategy objects:

Both choices are independent — any sampling can be paired with any density estimator:

extractor = InterfaceExtractor(
    sampling=SpaceSampling.rays(
        delta_azimuthal=20.0, delta_polar=8.0,
    ),
    density=DensityEstimator.gaussian(density_sigma=3.0),
)

The pairing between the chosen extractor and the analyzer’s SurfaceFitter is validated at TrajectoryAnalyzer construction via InterfaceExtractor.validate_compatibility(), which forwards to SpaceSampling.validate_compatibility().

class wetting_angle_kit.analysis.interface.InterfaceExtractor(sampling: SpaceSampling, density: DensityEstimator)[source]

Bases: object

Composes a sampling layout with a density estimator.

Parameters:

Examples

>>> from wetting_angle_kit.analysis import (
...     DensityEstimator, InterfaceExtractor, SpaceSampling,
... )
>>> extractor = InterfaceExtractor(
...     sampling=SpaceSampling.rays(
...         delta_azimuthal=20.0, delta_polar=8.0,
...     ),
...     density=DensityEstimator.gaussian(density_sigma=3.0),
... )
extract(liquid_coordinates: ndarray, center_geom: ndarray, droplet_geometry: DropletGeometry, surface_kind: Literal['slicing', 'whole']) list[ndarray] | ndarray[source]

Build the interface point set for one batch.

Delegates to SpaceSampling.extract(), threading self.density through.

property sampling_kind: Literal['rays', 'grid']

Tag identifying the sampling layout ("rays" or "grid").

validate_compatibility(surface_kind: Literal['slicing', 'whole'], droplet_geometry: DropletGeometry) None[source]

Raise if this extractor cannot serve (surface_kind, geometry).

Forwards to SpaceSampling.validate_compatibility(); the sampling owns the validation rules (e.g. delta_azimuthal is required for slicing-spherical rays).

class wetting_angle_kit.analysis.interface.SpaceSampling[source]

Bases: ABC

Strategy interface for space-sampling layouts.

Concrete instances come from one of the classmethod factories rays() or grid(); the abstract extract() and validate_compatibility() methods are dispatched by the composing InterfaceExtractor after pooling atom positions.

abstractmethod extract(liquid_coordinates: ndarray, center_geom: ndarray, droplet_geometry: DropletGeometry, surface_kind: Literal['slicing', 'whole'], density: DensityEstimator) list[ndarray] | ndarray[source]

Build the interface point set for one batch.

Parameters:
  • liquid_coordinates (ndarray, shape (N, 3)) – Pooled liquid-atom coordinates in the internal frame.

  • center_geom (ndarray, shape (3,)) – Geometric droplet center.

  • droplet_geometry (DropletGeometry) – Droplet symmetry; drives the per-slice axis choice for slicing modes and the ray-fan / grid layout for whole modes.

  • surface_kind ({"slicing", "whole"}) – What the downstream SurfaceFitter will consume.

  • density (DensityEstimator) – Density-estimation strategy. The sampling delegates per-cell or per-ray-sample density to this strategy.

Returns:

list[ndarray] of (M_i, 2) per-slice points when surface_kind="slicing"; a single (N, 3) shell when surface_kind="whole".

Return type:

InterfaceData

classmethod grid(*, grid_params: dict[str, Any] | None = None, delta_azimuthal: float | None = 15.0, delta_cylinder: float | None = None) SpaceSampling[source]

Fixed-cell grid sampling layout.

Per-slice in slicing mode: spherical droplets iterate over azimuthal angles γ [0°, 180°) controlled by delta_azimuthal; cylindrical droplets iterate over axial steps controlled by delta_cylinder. Each slice produces an (s, z) density grid and one iso-contour. Whole mode builds a 3D (x, y, z) grid centred laterally on the droplet COM and runs marching cubes.

Parameters:
  • grid_params (dict, optional) – Grid spec. For slicing, six keys: "xi_0", "xi_f", "dx", "zi_0", "zi_f", "dz". xi_0 should be negative for a centred slice that spans both halves of the diameter. For whole, add three more: "yi_0", "yi_f", "dy" (xi/yi grids are in the droplet-centred lateral frame; zi stays in the lab frame). If None (default), the grid is auto-derived per batch from the atom bounding box plus a 5 Å buffer, with cell width set to density_sigma / 2 for Gaussian or 2 Å for binning.

  • delta_azimuthal (float or None, default 15.0) – Azimuthal step (degrees) between slicing planes for slicing + spherical. Ignored for cylinder geometries and whole-fit modes.

  • delta_cylinder (float, optional) – Step (Å) along the cylinder axis between slicing planes for slicing + cylinder. Required for that case; ignored otherwise.

classmethod rays(*, delta_azimuthal: float | None = 15.0, delta_cylinder: float | None = None, n_rays_sphere: int | None = None, delta_polar: float = 8.0, points_per_angstrom: float = 1.0) SpaceSampling[source]

Ray-fan sampling layout.

Required ray-fan parameters depend on the (surface_kind, droplet_geometry) the sampling is paired with:

surface_kind, geometry

required ray params

slicing, spherical

[delta_azimuthal] (+ delta_polar)

slicing, cylinder_x/y

delta_cylinder (+ delta_polar)

whole, spherical

n_rays_sphere

whole, cylinder_x/y

delta_cylinder (+ delta_polar)

Parameters:
  • delta_azimuthal (float or None, default 15.0) – Azimuthal step (degrees) between slicing planes for the spherical slicing mode. None disables the parameter (useful when only cylinder modes are needed).

  • delta_cylinder (float, optional) – Step (Å) along the cylinder axis between slices for the cylinder modes (both slicing and whole).

  • n_rays_sphere (int, optional) – Total number of rays covering the full sphere for the spherical whole-fit mode. Rays are placed via an equal-area Fibonacci (cos θ, φ) construction so the angular density is uniform from south to north pole. Full-sphere (rather than upper-hemisphere) coverage is intentional: downward rays from the droplet COM traverse the liquid and hit the wall plane, producing interface points at the wall — that keeps WallDetector.min_plus_offset() consistent with the physical wall position.

  • delta_polar (float, default 8.0) – In-plane ray step (degrees) for every mode that emits rays in the (x, z) plane (i.e. everything except whole + spherical).

  • points_per_angstrom (float, default 1.0) – Sampling density along each ray (samples per Å).

abstractmethod validate_compatibility(surface_kind: Literal['slicing', 'whole'], droplet_geometry: DropletGeometry) None[source]

Raise if this sampling cannot serve (surface_kind, geometry).

Called by TrajectoryAnalyzer.__init__ so misconfigurations fail fast at construction instead of at the first batch.

Surface fitters: derive a contact angle from interface points + wall.

A SurfaceFitter consumes an interface point set produced by an InterfaceExtractor plus a wall z-coordinate produced by a WallDetector, and returns one BatchResult per call holding the contact angle and fit diagnostics.

Two fitter kinds are supported:

  • slicing: one algebraic-circle fit per slice in the slice’s (x, z) plane, then a mean across slices.

  • whole: one algebraic-sphere fit (spherical droplet) or algebraic-cylinder fit (cylindrical droplet) to the 3D interface shell.

Users construct fitters through classmethod factories on the base class:

SurfaceFitter.slicing()
SurfaceFitter.whole(bootstrap_samples=0)
class wetting_angle_kit.analysis.fitters.FitOutput[source]

Bases: ABC

Frames-less per-batch fit output returned by SurfaceFitter.fit().

The fitter computes the geometric fit and returns one of SlicingFitOutput or WholeFitOutput. The analyzer then calls to_batch_result() with the batch’s frame indices to produce the user-facing BatchResult — keeping bookkeeping (frames) and computation (the fit) separate.

abstractmethod to_batch_result(frames: list[int]) BatchResult[source]

Attach frames to this fit output and return a BatchResult.

class wetting_angle_kit.analysis.fitters.SlicingFitOutput(*, angle: float, z_wall: float, rms_residual: float, angle_std: float, per_slice_angles: ndarray, slice_surfaces: list[ndarray], slice_popts: ndarray, n_slices_total: int, n_slices_used: int)[source]

Bases: FitOutput

Output of SurfaceFitter.slicing() for one batch.

Carries the same payload as SlicingBatchResult minus frames. Field semantics are identical; see that class for documentation.

to_batch_result(frames: list[int]) SlicingBatchResult[source]

Attach frames to this fit output and return a BatchResult.

class wetting_angle_kit.analysis.fitters.SurfaceFitter[source]

Bases: ABC

Abstract base for contact-angle surface fitters.

Concrete fitters are constructed through the classmethod factories slicing() and whole(). Direct subclassing is supported for custom strategies but the factories cover all built-in cases.

abstractmethod fit(interface_data: list[ndarray] | ndarray, z_wall: float, droplet_geometry: DropletGeometry) FitOutput[source]

Fit the contact angle for one batch.

Parameters:
  • interface_data (InterfaceData) – Interface point set produced by the InterfaceExtractor. Per-slice 2D points for kind="slicing"; a 3D shell for kind="whole".

  • z_wall (float) – Wall-plane z-coordinate from the WallDetector.

  • droplet_geometry (DropletGeometry) – Droplet symmetry; controls the geometric model (circle per slice / sphere / cylinder).

Returns:

SlicingFitOutput for slicing fitters, WholeFitOutput for whole fitters. The analyzer attaches the batch’s frame indices via FitOutput.to_batch_result() to produce the user-facing BatchResult.

Return type:

FitOutput

kind: ClassVar[Literal['slicing', 'whole']]

Surface-representation kind this fitter consumes. Set by each concrete subclass; the analyzer matches this against the chosen InterfaceExtractor at construction time.

classmethod slicing(*, surface_filter_offset: float = 2.0) SurfaceFitter[source]

Per-slice algebraic circle fits, averaged across slices.

Each slice’s 2D interface points are filtered to z > z_wall + surface_filter_offset (to exclude wall-adjacent density distortions), an algebraic Taubin circle is fit to the kept points, and the contact angle is the angle of intersection between that circle and the line z = z_wall. The batch angle is the mean over slices; BatchResult.angle_std is the empirical std across slices.

Parameters:

surface_filter_offset (float, default 2.0) – Vertical offset above z_wall (Å) below which interface points are excluded from the circle fit. This is distinct from any offset baked into the WallDetector: this offset is a fit-quality knob for the per-slice circle, and the wall detector’s offset (if it uses one, e.g. WallDetector.min_plus_offset()) defines where the wall plane sits.

abstractmethod validate_compatibility(droplet_geometry: DropletGeometry) None[source]

Raise if this fitter cannot handle droplet_geometry.

Called by TrajectoryAnalyzer.__init__. The kind compatibility (slicing vs whole) is enforced separately at the analyzer level by matching SurfaceFitter.kind against the extractor’s chosen surface_kind.

classmethod whole(*, surface_filter_offset: float = 2.0, bootstrap_samples: int = 0) SurfaceFitter[source]

Algebraic sphere or cylinder fit to the 3D interface shell.

Spherical droplets get a sphere fit; cylindrical droplets get a circular-cylinder fit whose axis is parallel to y (internal frame, post axis-swap for cylinder_x). The contact angle follows from the cap geometry: cos θ = (z_wall - z_center) / R.

Parameters:
  • surface_filter_offset (float, default 2.0) – Vertical offset above z_wall (Å) below which shell points are excluded from the geometric fit. Same role as in slicing(): distinct from the wall detector’s offset.

  • bootstrap_samples (int, default 0) – If positive, the fit is repeated on this many bootstrap resamples of the filtered shell, and the resulting std of the angles is reported as BatchResult.angle_std. 0 disables bootstrap; the field is then None in the returned WholeBatchResult.

class wetting_angle_kit.analysis.fitters.WholeFitOutput(*, angle: float, z_wall: float, rms_residual: float, angle_std: float | None, interface_shell: ndarray, popt: ndarray)[source]

Bases: FitOutput

Output of SurfaceFitter.whole() for one batch.

Carries the same payload as WholeBatchResult minus frames. Field semantics are identical; see that class for documentation.

to_batch_result(frames: list[int]) WholeBatchResult[source]

Attach frames to this fit output and return a BatchResult.

Wall-plane detectors used by trajectory analyzers.

A WallDetector returns the z-coordinate of the wall plane that a SurfaceFitter intersects to compute the contact angle. Three strategies are supported:

  • min_plus_offset: take the lowest interface point and shift up by a configurable offset. Cheap and self-contained but picks up thermal noise from the liquid–vapor interface bottom.

  • explicit: use a fixed user-supplied z value. Best when the wall plane is known a priori from the simulation setup.

  • from_atoms: derive z from a pool of wall atom positions (e.g. mean z of the topmost layer). Most physical but requires the analyzer to be told which atoms form the wall.

Users construct detectors through classmethod factories on the base class:

WallDetector.min_plus_offset(offset=2.0)
WallDetector.explicit(z_wall=15.0)
WallDetector.from_atoms(wall_atom_indices=indices)
class wetting_angle_kit.analysis.wall.WallContext(interface_data: list[ndarray] | ndarray, wall_coords: ndarray | None = None)[source]

Bases: object

Per-batch data passed to WallDetector.detect().

Wrapping the inputs in a single object keeps the detector method signature forward-compatible: new detectors can read new fields without changing the protocol.

interface_data

Interface point set produced by the InterfaceExtractor; format depends on the extractor kind (per-slice 2D points or a 3D shell).

Type:

list[ndarray] or ndarray

wall_coords

Pooled (N, 3) positions of wall atoms in the internal coordinate frame, if the analyzer was constructed with wall_atom_indices. Required by from_atoms detectors and unused by the others.

Type:

ndarray, optional

class wetting_angle_kit.analysis.wall.WallDetector[source]

Bases: ABC

Abstract base for wall-plane detection strategies.

Construct concrete detectors with one of the classmethod factories min_plus_offset(), explicit(), or from_atoms(). Direct subclassing is supported for custom strategies but the factories cover all built-in cases.

abstractmethod detect(ctx: WallContext) float[source]

Return the wall-plane z-coordinate for one batch.

Parameters:

ctx (WallContext) – Per-batch data; see WallContext.

Returns:

Wall-plane z in the internal coordinate frame (Å).

Return type:

float

classmethod explicit(z_wall: float) WallDetector[source]

Use a fixed wall z-coordinate.

Parameters:

z_wall (float) – Wall-plane z in the internal coordinate frame (Å).

classmethod from_atoms(wall_atom_indices: ndarray, method: Literal['max_z', 'mean_top_layer'] = 'mean_top_layer', top_layer_tolerance: float = 1.0) WallDetector[source]

Derive wall z from a set of wall atom positions.

The analyzer must be constructed with the matching wall_atom_indices so the wall atoms are gathered and supplied through WallContext.wall_coords.

Parameters:
  • wall_atom_indices (ndarray) – Indices of the atoms that form the wall.

  • method ({"max_z", "mean_top_layer"}, default "mean_top_layer") – How to reduce wall atom z values to a single plane. "max_z" uses the highest wall atom z; cheap but noisy. "mean_top_layer" averages over all atoms within top_layer_tolerance Å of the maximum, smoothing thermal motion.

  • top_layer_tolerance (float, default 1.0) – Vertical window (Å) defining the “top layer” for method="mean_top_layer". Ignored for "max_z".

classmethod min_plus_offset(offset: float = 2.0) WallDetector[source]

Take the lowest interface point and shift up by offset.

Parameters:

offset (float, default 2.0) – Vertical shift (Å) added to the minimum z to skip the wall-adjacent density spike. The default of 2.0 Å matches the slicing analyzer’s historical behaviour for water on silica-like surfaces; tune for other systems.

Temporal aggregation across frames for trajectory analysis.

A TemporalAggregator groups frame indices into batches. Each batch is later processed by an analyzer as a single fitting unit, producing one contact angle estimate per batch.

The batch_size parameter controls the time-vs-statistics trade-off:

  • batch_size=1 (default) — per-frame analysis. Produces a time series with one angle per frame; statistics come from frame-to-frame variation.

  • batch_size=N (N > 1) — pool consecutive groups of N frames together before fitting. Reduces thermal noise per fit at the cost of time resolution.

  • batch_size=-1 — fully pooled. Every requested frame goes into a single batch, producing one angle estimate for the trajectory.

class wetting_angle_kit.analysis.temporal.TemporalAggregator(batch_size: int = 1)[source]

Bases: object

Group frame indices into batches for per-batch surface fitting.

Designed to be held by a TrajectoryAnalyzer and driven from inside analyze(), which supplies the frame indices to walk. Standalone use is fine for inspection (e.g. previewing batch boundaries) but the caller must always provide frame_range.

Parameters:

batch_size (int, default 1) – Number of consecutive frames pooled per surface fit. batch_size=1 (the default) gives per-frame analysis: each frame is its own batch. Larger values pool consecutive groups of frames, trading time resolution for statistics; the last batch is shorter if the range isn’t evenly divisible. batch_size=-1 is the “all” sentinel: every supplied frame is pooled into a single batch.

iter_batches(frame_range: list[int]) Iterator[list[int]][source]

Yield successive lists of frame indices, one per fitting unit.

Parameters:

frame_range (list[int]) – The frame indices to distribute. The analyzer normally populates this with range(parser.frame_count()) or with a caller-supplied subset; the aggregator only groups what it is given and never consults the parser itself. May be empty (no batches yielded).

Yields:

list[int] – One batch of frame indices. Order within and across batches preserves the order of frame_range.

n_batches(n_frames: int) int[source]

Return the number of batches that would be yielded.

Useful for sizing progress bars before iteration starts.

Parameters:

n_frames (int) – Length of the frame_range that would be passed to iter_batches().

Returns:

Number of batches the aggregator will produce for that input.

Return type:

int

Droplet symmetry and the internal axis convention.

Every analyzer in wetting_angle_kit.analysis operates on a DropletGeometry instance. The class normalises the three supported cases (spherical, cylinder_x, cylinder_y) and exposes a single helper, to_internal_coords(), that downstream code can use to assume the cylinder axis is always y.

User-facing APIs accept either a DropletGeometry instance or the bare string name; DropletGeometry.coerce() is the canonical entry point that performs the conversion.

class wetting_angle_kit.analysis.geometry.DropletGeometry(name: Literal['spherical', 'cylinder_x', 'cylinder_y'])[source]

Bases: object

Droplet symmetry descriptor with axis-layout helpers.

Three cases are supported:

  • spherical: the droplet is a 3D cap with no preferred horizontal axis. Rays sweep over the upper hemisphere (theta, phi).

  • cylinder_y: the droplet is a ridge whose translational symmetry axis is y. In-plane analysis happens in (x, z) and slices are taken at successive y positions. No internal axis swap.

  • cylinder_x: the droplet is a ridge whose translational symmetry axis is x. A [1, 0, 2] swap is applied at the analyzer boundary so every downstream routine can assume the cylinder axis is y internally. The swap is self-inverse, so the same helper maps internal coordinates back to user coordinates.

Picking cylinder_x vs cylinder_y

Pick the one whose name matches your trajectory’s lab-frame axis along which the ridge is invariant:

  • If your dump file’s atoms are uniformly distributed along y (i.e. the simulation box’s y direction is the periodic cylinder axis), pass "cylinder_y".

  • If the same situation holds along x instead, pass "cylinder_x".

The two are not interchangeable — picking the wrong one is the cylinder analogue of confusing the in-plane radial axis with the cylinder axis. Symptoms of a mismatch: the slicing fitter iterates over the wrong axis (slicing planes go across the ridge instead of along it), so each “slice” sees almost no atoms and the per-slice circle fit either NaNs out or recovers a non-physical angle.

Internally everything happens in the cylinder_y frame: cylinder_x simply applies a self-inverse x↔y column swap at the parser/analyzer boundary so all downstream extractors, fitters, and visualisers can assume the cylinder axis is y. No analysis logic is duplicated between the two cases — they’re distinguished only by where the swap is (or isn’t) applied.

If you’re not sure which axis your trajectory uses, the safe diagnostic is to load one frame, plot atom positions, and look at which lateral coordinate the droplet spans the full box.

classmethod coerce(value: DropletGeometry | Literal['spherical', 'cylinder_x', 'cylinder_y'] | str) DropletGeometry[source]

Return a DropletGeometry for either an instance or a name.

Parameters:

value (DropletGeometry or str) – Either an existing instance (returned unchanged) or one of the bare name strings "spherical", "cylinder_x", "cylinder_y".

Return type:

DropletGeometry

property cylinder_axis: Literal['x', 'y'] | None

User-frame axis along which the cylinder extends, or None.

to_internal_coords(coords: ndarray) ndarray[source]

Map coordinates from the user frame to the internal frame.

For cylinder_x this applies the [1, 0, 2] swap so the cylinder axis ends up on the y column. Spherical and cylinder_y are returned unchanged. Accepts any array whose last axis has length 3 (a single point (3,) or a batch (..., 3)).

to_user_coords(coords: ndarray) ndarray[source]

Map coordinates from the internal frame back to the user frame.

Mirror of to_internal_coords(): applies the [1, 0, 2] swap for cylinder_x (which is its own inverse), and returns the input unchanged for spherical and cylinder_y.

wetting_angle_kit.analysis.geometry.DropletGeometryName

Public type alias for the three accepted droplet geometry names.

alias of Literal[‘spherical’, ‘cylinder_x’, ‘cylinder_y’]

Results dataclasses

In-memory containers for trajectory analyzer outputs.

The unified TrajectoryAnalyzer returns TrajectoryResults holding one BatchResult per batch produced by the TemporalAggregator. The specific BatchResult subclass depends on the SurfaceFitter kind:

The two coupled-fit analyzers — CoupledFit2DAnalyzer and CoupledFit3DAnalyzer — each return their own results type (CoupledFit2DResults, CoupledFit3DResults). They carry density grids plus coupled-fit parameters and are therefore not part of the TrajectoryResults hierarchy.

class wetting_angle_kit.analysis.results.BatchResult(*, frames: list[int], angle: float, z_wall: float, rms_residual: float, angle_std: float | None = None)[source]

Bases: object

Common fields shared by all per-batch trajectory results.

All fields are keyword-only so that subclasses can interleave their own fields with the parent’s defaulted ones without ordering constraints.

frames

Frame indices pooled into this batch.

Type:

list[int]

angle

Representative contact angle for the batch (degrees). For slicing fits this is the mean across slices; for whole fits it is the single fitted angle.

Type:

float

z_wall

Wall-plane z used by the surface fitter for this batch (Å).

Type:

float

rms_residual

Aggregate fit residual (Å). For slicing fits, an aggregate of the per-slice circle-fit residuals; for whole fits, the single sphere/cylinder-fit residual.

Type:

float

angle_std

Within-batch standard deviation of the contact angle (degrees), describing the spread of the per-batch angle. For slicing fits, the nanstd of per_slice_angles (always populated). For whole fits, the bootstrap std when the fitter was constructed with bootstrap_samples > 0, otherwise None.

Type:

float, optional

class wetting_angle_kit.analysis.results.CoupledFit2DBatchResult(frames: list[int], angle: float, model_params: dict[str, float], xi_grid: ndarray, zi_grid: ndarray, density: ndarray)[source]

Bases: object

Per-batch result from CoupledFit2DAnalyzer.

frames

Frame indices pooled into this batch.

Type:

list[int]

angle

Contact angle (degrees) from the 2D coupled tanh-model fit.

Type:

float

model_params

Fitted parameters of the 2D hyperbolic tangent model; keys are "rho1", "rho2", "R_eq", "zi_c", "zi_0", "t1", "t2".

Type:

dict[str, float]

xi_grid

In-plane grid-cell centers (Å).

Type:

ndarray

zi_grid

Vertical grid-cell centers (Å).

Type:

ndarray

density

(len(xi_grid), len(zi_grid)) density sampled on the grid.

Type:

ndarray

class wetting_angle_kit.analysis.results.CoupledFit2DResults(batches: list[~wetting_angle_kit.analysis.results.CoupledFit2DBatchResult], method_metadata: dict[str, ~typing.Any] = <factory>)[source]

Bases: _AngleResultsMixin

In-memory results of a CoupledFit2DAnalyzer.analyze run.

batches

Per-batch results, in the order produced by the aggregator.

Type:

list[CoupledFit2DBatchResult]

method_metadata

Free-form descriptor (droplet geometry, grid params, initial parameters, batch size).

Type:

dict

class wetting_angle_kit.analysis.results.CoupledFit3DBatchResult(frames: list[int], angle: float, model_params: dict[str, float], xi_grid: ndarray, yi_grid: ndarray, zi_grid: ndarray, density: ndarray)[source]

Bases: object

Per-batch result from CoupledFit3DAnalyzer.

Only meaningful for spherical droplets; cylindrical droplets carry a translational symmetry along the cylinder axis that the 2D analyzer already exploits, so CoupledFit3DAnalyzer rejects non-spherical geometries at construction.

frames

Frame indices pooled into this batch.

Type:

list[int]

angle

Contact angle (degrees) from the 3D coupled tanh-model fit.

Type:

float

model_params

Fitted parameters of the 3D hyperbolic tangent model; keys are "rho1", "rho2", "R_eq", "xi_c", "yi_c", "zi_c", "zi_0", "t1", "t2". The droplet horizontal centers xi_c / yi_c are reported even if the underlying fit fixes them to zero by symmetry.

Type:

dict[str, float]

xi_grid

x grid-cell centers (Å).

Type:

ndarray

yi_grid

y grid-cell centers (Å).

Type:

ndarray

zi_grid

Vertical grid-cell centers (Å).

Type:

ndarray

density

(len(xi_grid), len(yi_grid), len(zi_grid)) density sampled on the 3D grid.

Type:

ndarray

class wetting_angle_kit.analysis.results.CoupledFit3DResults(batches: list[~wetting_angle_kit.analysis.results.CoupledFit3DBatchResult], method_metadata: dict[str, ~typing.Any] = <factory>)[source]

Bases: _AngleResultsMixin

In-memory results of a CoupledFit3DAnalyzer.analyze run.

batches

Per-batch results, in the order produced by the aggregator.

Type:

list[CoupledFit3DBatchResult]

method_metadata

Free-form descriptor (droplet geometry — always spherical for this analyzer, grid params, initial parameters, batch size).

Type:

dict

class wetting_angle_kit.analysis.results.SlicingBatchResult(*, frames: list[int], angle: float, z_wall: float, rms_residual: float, angle_std: float | None = None, per_slice_angles: ndarray, slice_surfaces: list[ndarray], slice_popts: ndarray, n_slices_total: int, n_slices_used: int)[source]

Bases: BatchResult

Per-batch result from a slicing-kind surface fitter.

All per-slice arrays are full length (one entry per attempted slice) and index-aligned: a slice that produced no valid contact angle (empty, too few points, degenerate circle fit, or a circle that does not reach the wall) is marked nan rather than dropped, so attrition is visible and the slice index is preserved.

The inherited BatchResult.angle field stores the mean across slices (nanmean). Use median_angle for the median, which is more robust to outlier slices.

per_slice_angles

(n_slices_total,) array of per-slice contact angles (degrees), with nan for slices that produced no angle. BatchResult.angle is nanmean of this array and BatchResult.angle_std its nanstd.

Type:

ndarray

slice_surfaces

One (M_i, 2) array per slice of interface points in the slice (x, z) plane (kept for every slice, including those that produced no angle).

Type:

list[ndarray]

slice_popts

(n_slices_total, 4) array of fitted circle parameters per slice; columns [xc, zc, R, z_wall]. Rows for slices with no valid fit are nan.

Type:

ndarray

n_slices_total

Number of slices the extractor produced for this batch.

Type:

int

n_slices_used

Number of those slices that produced a valid contact angle (the count of non-nan entries in per_slice_angles). n_slices_used < n_slices_total signals per-slice attrition.

Type:

int

property median_angle: float

Median contact angle across slices (degrees).

More robust than angle (the mean) when one or two slices are outliers — e.g. due to asymmetric density near the periodic boundary. nan slices are ignored.

class wetting_angle_kit.analysis.results.TrajectoryResults(batches: list[~wetting_angle_kit.analysis.results.BatchResult], method_metadata: dict[str, ~typing.Any] = <factory>)[source]

Bases: _AngleResultsMixin

In-memory results of a TrajectoryAnalyzer.analyze run.

Holds one BatchResult per batch produced by the TemporalAggregator. Within a single Results object all batches share the same subclass (SlicingBatchResult or WholeBatchResult), determined by the analyzer’s SurfaceFitter kind.

batches

Per-batch results, in the order produced by the aggregator.

Type:

list[BatchResult]

method_metadata

Free-form descriptor of the analyzer configuration (kind, droplet geometry, batch size, …) for downstream plotting / serialization.

Type:

dict

class wetting_angle_kit.analysis.results.WholeBatchResult(*, frames: list[int], angle: float, z_wall: float, rms_residual: float, angle_std: float | None = None, interface_shell: ndarray, popt: ndarray)[source]

Bases: BatchResult

Per-batch result from a whole-kind surface fitter.

interface_shell

(N, 3) array of interface points used in the fit, in the internal (x, y, z) frame.

Type:

ndarray

popt

Fitted shape parameters extended by the wall plane. Spherical fitter: [xc, yc, zc, R, z_wall]. Cylinder fitter: [xc, zc, R, z_wall].

Type:

ndarray

Visualisation

class wetting_angle_kit.visualization.AngleEvolutionPlotter(results: Any, *, label: str = 'trajectory', timestep: float = 1.0, time_unit: str = 'ps', stat: Literal['median', 'mean'] = 'median', method_name: str | None = None)[source]

Bases: BaseTrajectoryPlotter

Plot per-batch contact-angle evolution across a trajectory.

Parameters:
  • results – A *Results object exposing .batches with .angle and .frames (i.e. TrajectoryResults, CoupledFit2DResults, or CoupledFit3DResults). For per-frame analyses use TemporalAggregator with batch_size=1; for pooled batches the evolution is shown per batch with each x-value at the pooled-frames midpoint.

  • label (str, default "trajectory") – Display label used in legend entries and in TrajectoryStats.

  • timestep (float, default 1.0) – Time between two consecutive frames in the trajectory file (dump interval × MD integration timestep, not the integration timestep itself). Applied to frames to produce the x-axis.

  • time_unit (str, default "ps") – Unit shown on the x-axis label.

  • stat ({“median”, “mean”}, default "median") – Per-batch central-tendency aggregation across slices for slicing results. Ignored for other result shapes (where only a single BatchResult.angle is defined).

  • method_name (str, optional) – Free-form method tag used in TrajectoryStats. Defaults to the underlying analyzer’s class name when inferable.

plot(*, per_frame_std: bool = True, running_mean: bool = True, title: str | None = None, save_path: str | None = None) Figure[source]

Build the angle-evolution figure.

Parameters:
  • per_frame_std (bool, default True) – If True, draw a transparent ±σ band around the per-batch curve using the within-batch standard deviation (per-slice scatter for slicing fits, bootstrap σ for whole fits, otherwise no band).

  • running_mean (bool, default True) – If True, overlay the cumulative running mean of the per-batch central tendency as a dashed line, plus a transparent ±σ band of that cumulative series.

  • title (str, optional) – Figure title. Defaults to a "Contact angle evolution ({stat})" string.

  • save_path (str, optional) – If provided, also write the figure to standalone HTML.

Returns:

Figure with the per-batch line (always), the within-batch band (when per_frame_std and the batches expose a finite angle_std), and the running mean line + its cumulative band (when running_mean).

Return type:

plotly.graph_objects.Figure

summary() list[TrajectoryStats][source]

Return per-trajectory summary statistics.

class wetting_angle_kit.visualization.BaseTrajectoryPlotter[source]

Bases: ABC

Abstract base for trajectory plotters.

Subclasses own their own data layout (per-method result containers, directories, etc.) and must implement summary() returning one TrajectoryStats per trajectory.

abstractmethod summary() list[TrajectoryStats][source]

Return per-trajectory summary statistics.

class wetting_angle_kit.visualization.DensityContourPlotter(source: Any, *, label: str = 'trajectory', colorscale: str = 'Jet')[source]

Bases: object

Plot a binned density field with the fitted cap and wall overlaid.

Parameters:
  • source – Single batch or full results object as listed in the module docstring.

  • label (str, default "trajectory") – Display label used in the figure title.

  • colorscale (str, default "Jet") – Plotly colorscale for the density contour.

plot(*, title: str | None = None, save_path: str | None = None) Figure[source]

Build the density contour figure.

Parameters:
  • title (str, optional) – Figure title. Defaults to a "Density field {label} (batch_descriptor)" string, where the batch descriptor names the batch when the source is a single batch and "averaged" when it is a full results object.

  • save_path (str, optional) – If provided, write the figure to standalone HTML.

Returns:

Contour + dashed fitted cap + dotted wall line.

Return type:

plotly.graph_objects.Figure

plot_3d_isosurface(*, n_levels: int = 10, show_fit: bool = True, title: str | None = None, save_path: str | None = None) Figure[source]

3D isosurface of the density field with a density-threshold slider.

Only accepts CoupledFit3DBatchResult or CoupledFit3DResults sources (the full 3D density grid is required).

Parameters:
  • n_levels (int, default 10) – Number of iso-density levels exposed in the slider.

  • show_fit (bool, default True) – If True, overlay the fitted sphere as a black dashed wireframe (meridians + parallels).

  • title (str, optional) – Figure title. Defaults to "Isosurface ρ ... {label}".

  • save_path (str, optional) – If provided, write the figure to standalone HTML.

Returns:

3D isosurface with a wall plane and a density slider.

Return type:

plotly.graph_objects.Figure

class wetting_angle_kit.visualization.DropletSlicePlotter(center: bool = True)[source]

Bases: object

Interactive Plotly slice visualization with toggleable layers.

plot_surface_points(oxygen_position: ndarray, surface_data: list[ndarray], popt: Sequence[float], wall_coords: ndarray, alpha: float | None = None, y_com: float | None = None, pbc_y: float | None = None, show_water: bool = True, show_surface: bool = True, show_circle: bool = True, show_tangent: bool = True, show_wall: bool = True) Any[source]

Create interactive Plotly figure for a single frame slice.

Parameters:
  • oxygen_position (ndarray (N, 3)) – Oxygen atom coordinates.

  • surface_data (list[array]) – List of surface contours for selected slice.

  • popt (sequence) – Fitted circle parameters (x_center, z_center, radius, extra).

  • wall_coords (ndarray (M, 3)) – Wall particle coordinates.

  • alpha (float, optional) – Contact angle for tangent construction.

  • y_com (float, optional) – Mean y used for slicing; computed if None.

  • pbc_y (float, optional) – Y box length for periodic slicing.

  • show_water (bool) – Layer visibility toggles.

  • show_surface (bool) – Layer visibility toggles.

  • show_circle (bool) – Layer visibility toggles.

  • show_tangent (bool) – Layer visibility toggles.

  • show_wall (bool) – Layer visibility toggles.

Returns:

Configured figure object (not saved).

Return type:

plotly.graph_objects.Figure

class wetting_angle_kit.visualization.TrajectoryStats(method_name: str, label: str, mean_surface_area: float, mean_contact_angle: float, std_contact_angle: float, n_samples: int)[source]

Bases: object

Summary statistics for a single contact-angle trajectory.

A plotter’s .summary() method returns this dataclass so callers can both display the block (print(stats)) and reuse the underlying numbers programmatically.

method_name

Name of the analysis method (e.g. "Slicing Analysis").

Type:

str

label

Display label identifying the trajectory.

Type:

str

mean_surface_area

Mean droplet/cap surface area in Ų.

Type:

float

mean_contact_angle

Mean contact angle in degrees.

Type:

float

std_contact_angle

Standard deviation of the contact angle in degrees.

Type:

float

n_samples

Number of samples (frames or batches) contributing to the means.

Type:

int