Examples
Ready-to-run example scripts demonstrating common workflows.
Parsing Trajectory Files
Parse different trajectory file formats (LAMMPS dump, ASE, XYZ) into a
unified (N, 3) coordinate array.
1"""
2Example: Using LammpsDumpParser and LammpsDumpWaterFinder
3
4This example shows how to:
51. Identify water molecules in a LAMMPS dump file.
62. Extract only their oxygen atom coordinates.
7"""
8
9from wetting_angle_kit.parsers import (
10 AseParser,
11 AseWaterFinder,
12 LammpsDumpParser,
13 LammpsDumpWaterFinder,
14 XYZParser,
15)
16
17# --- Define input file ---
18filename = "../../tests/trajectories/traj_10_3_330w_nve_4k_reajust.lammpstrj"
19
20# --- Initialize water molecule finder ---
21wat_find = LammpsDumpWaterFinder(
22 filename,
23 oxygen_type=1, # atom type for oxygen
24 hydrogen_type=2, # atom type for hydrogen
25)
26
27# --- Identify water oxygen indices for the first frame ---
28oxygen_indices = wat_find.get_water_oxygen_indices(frame_index=0)
29print(f"Number of water molecules: {len(oxygen_indices)}")
30
31# --- Initialize parser ---
32parser = LammpsDumpParser(filename)
33
34# --- Extract only oxygen coordinates for frame 0 ---
35# For LammpsDumpParser, `indices` are LAMMPS particle IDs (because LAMMPS may
36# reorder atoms between frames). For XYZParser/AseParser, `indices` are
37# 0-based positional indices.
38oxygen_positions = parser.parse(frame_index=0, indices=oxygen_indices)
39print("Extracted oxygen coordinates shape:", oxygen_positions.shape)
40
41# --- Optional: Extract all atoms ---
42# all_positions = parser.parse(frame_index=0)
43# print("All atom positions shape:", all_positions.shape)
44
45"""
46Example: Using AseParser and AseWaterFinder
47
48This example demonstrates how to:
491. Identify water oxygens in an ASE trajectory.
502. Extract their positions for a given frame.
51"""
52
53# --- Define input file ---
54filename = "../../tests/trajectories/slice_10_mace_mlips_cylindrical_2_5.traj"
55
56# --- Initialize water molecule finder ---
57wat_find = AseWaterFinder(
58 filename,
59 oh_cutoff=1.2, # O–H bond cutoff (Å); ASE NeighborList handles the
60 # per-atom splitting internally now.
61)
62
63# --- Get oxygen indices for frame 0 ---
64oxygen_indices = wat_find.get_water_oxygen_indices(frame_index=0)
65print(f"Number of water molecules: {len(oxygen_indices)}")
66
67# --- Initialize parser ---
68parser = AseParser(filename)
69
70# --- Extract oxygen coordinates only ---
71oxygen_positions = parser.parse(frame_index=0, indices=oxygen_indices)
72print("Extracted oxygen coordinates shape:", oxygen_positions.shape)
73
74"""
75Example: Using XYZParser
76
77This example demonstrates how to:
781. Load atomic positions from an XYZ file.
792. Extract all atoms or a subset of atoms.
80"""
81
82# --- Define input file ---
83filename = "../../tests/trajectories/slice_10_mace_mlips_cylindrical_2_5.xyz"
84
85# --- Initialize parser ---
86xyz_parser = XYZParser(filename)
87
88# --- Extract all atom coordinates for frame 0 ---
89positions = xyz_parser.parse(frame_index=0)
90print("Total atoms loaded:", len(positions))
91
92# --- Extract subset of atoms (first 50) ---
93subset = xyz_parser.parse(frame_index=0, indices=list(range(24)))
94print("Subset (50 atoms) shape:", subset.shape)
Slicing-Pipeline Contact Angle
Per-frame angle via the composable TrajectoryAnalyzer with the
ray-fan extractor and the slicing fitter.
1"""Slicing-pipeline contact-angle example.
2
3Runs the per-frame slicing-fit pipeline (ray-fan extractor + algebraic
4circle fitter + interface-derived wall) on a LAMMPS dump file and prints
5the recovered mean contact angle.
6"""
7
8from wetting_angle_kit.analysis import (
9 DensityEstimator,
10 InterfaceExtractor,
11 SpaceSampling,
12 SurfaceFitter,
13 TrajectoryAnalyzer,
14 WallDetector,
15)
16from wetting_angle_kit.analysis.temporal import TemporalAggregator
17from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder
18
19# --- Step 1: Define the trajectory file ---
20filename = "../../tests/trajectories/traj_spherical_drop_4k.lammpstrj"
21
22# --- Step 2: Identify the water-oxygen atoms ---
23wat_find = LammpsDumpWaterFinder(
24 filename,
25 oxygen_type=1,
26 hydrogen_type=2,
27)
28oxygen_indices = wat_find.get_water_oxygen_indices(frame_index=0)
29print("Number of water molecules:", len(oxygen_indices))
30
31# --- Step 3: Build the trajectory analyzer ---
32# Strategies: ray-fan Gaussian extractor + slicing fitter +
33# interface-derived wall + per-frame batching.
34analyzer = TrajectoryAnalyzer(
35 parser=LammpsDumpParser(filename),
36 atom_indices=oxygen_indices,
37 droplet_geometry="spherical",
38 interface_extractor=InterfaceExtractor(
39 sampling=SpaceSampling.rays(
40 delta_azimuthal=20.0, # 20° between slicing planes
41 delta_polar=8.0, # 8° in-plane ray step
42 ),
43 density=DensityEstimator.gaussian(),
44 ),
45 surface_fitter=SurfaceFitter.slicing(surface_filter_offset=2.0),
46 wall_detector=WallDetector.min_plus_offset(offset=0.0),
47 temporal_aggregator=TemporalAggregator(batch_size=1),
48)
49
50# --- Step 4: Run analysis on a frame range ---
51results = analyzer.analyze([1])
52print("Mean contact angle (°):", results.mean_angle)
53print("Std across batches (°):", results.std_angle)
54
55# Per-batch detail:
56batch = results.batches[0]
57print(
58 f"Frame {batch.frames[0]}: angle = {batch.angle:.2f}°, "
59 f"per-slice σ = {batch.angle_std:.2f}°, "
60 f"rms residual = {batch.rms_residual:.2f} Å"
61)
Whole-Fit Contact Angle with Bootstrap
Whole-shape sphere fit with the wall position taken from the actual substrate atoms and a bootstrap uncertainty.
1"""Whole-shape fit contact-angle example.
2
3Runs the whole-fit pipeline (full-sphere Fibonacci ray fan + algebraic
4sphere fit + wall atoms from the trajectory) on a LAMMPS dump file,
5with 100 bootstrap resamples for the angle uncertainty.
6"""
7
8from wetting_angle_kit.analysis import (
9 DensityEstimator,
10 InterfaceExtractor,
11 SpaceSampling,
12 SurfaceFitter,
13 TrajectoryAnalyzer,
14 WallDetector,
15)
16from wetting_angle_kit.parsers import (
17 LammpsDumpParser,
18 LammpsDumpWallParser,
19 LammpsDumpWaterFinder,
20)
21
22# --- Step 1: Define the trajectory file ---
23filename = "../../tests/trajectories/traj_spherical_drop_4k.lammpstrj"
24
25# --- Step 2: Identify water-oxygen and wall-atom indices ---
26wat_find = LammpsDumpWaterFinder(filename, oxygen_type=1, hydrogen_type=2)
27oxygen_indices = wat_find.get_water_oxygen_indices(frame_index=0)
28
29# Wall parser: ``liquid_particle_types`` lists the liquid types to EXCLUDE.
30wall_parser = LammpsDumpWallParser(filename, liquid_particle_types=[1, 2])
31carbon_indices = wall_parser.parse(frame_index=0)
32
33# --- Step 3: Build the whole-fit analyzer ---
34# Strategies: full-sphere Fibonacci ray fan + sphere fit + from_atoms wall.
35analyzer = TrajectoryAnalyzer(
36 parser=LammpsDumpParser(filename),
37 atom_indices=oxygen_indices,
38 droplet_geometry="spherical",
39 interface_extractor=InterfaceExtractor(
40 sampling=SpaceSampling.rays(n_rays_sphere=400),
41 density=DensityEstimator.gaussian(density_sigma=3.0),
42 ),
43 surface_fitter=SurfaceFitter.whole(
44 surface_filter_offset=3.0,
45 bootstrap_samples=100,
46 ),
47 wall_detector=WallDetector.from_atoms(
48 wall_atom_indices=carbon_indices,
49 method="mean_top_layer",
50 top_layer_tolerance=1.0,
51 ),
52 wall_atom_indices=carbon_indices,
53)
54
55# --- Step 4: Run the analysis ---
56batch = analyzer.analyze([1]).batches[0]
57print(f"angle = {batch.angle:.2f}° ± {batch.angle_std:.2f}° (bootstrap)")
58print(f"R = {batch.popt[3]:.2f} Å, z_wall = {batch.z_wall:.2f} Å")
59print(f"rms residual on the shell = {batch.rms_residual:.2f} Å")
Coupled-Fit Contact Angle
Coupled hyperbolic-tangent density-model fit via
CoupledFit2DAnalyzer — one angle per pooled batch. The
example shows both density estimators (histogram default vs Gaussian
KDE).
1"""Coupled-fit contact-angle example.
2
3Runs the coupled hyperbolic-tangent fit on a 2D density grid via
4:class:`CoupledFit2DAnalyzer`. The analyzer solves interface extraction,
5wall detection, and surface fitting together — one robust angle per pooled
6batch.
7
8Two density estimators are shown:
9
10- :meth:`DensityEstimator.binning` (the default) — top-hat histogram
11 with geometry-aware ``dV`` normalisation. Fast and exact; intrinsically
12 noisy at low per-cell counts.
13- :meth:`DensityEstimator.gaussian` — 3D Gaussian KDE on the cell
14 centres. Smooth density field with no per-cell Poisson noise; the
15 estimator of choice when running per-frame analyses or on systems
16 with low atom density per cell.
17"""
18
19from wetting_angle_kit.analysis import (
20 CoupledFit2DAnalyzer,
21 DensityEstimator,
22)
23from wetting_angle_kit.analysis.temporal import TemporalAggregator
24from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder
25
26# --- Step 1: Define the trajectory file ---
27filename = "../../tests/trajectories/traj_spherical_drop_4k.lammpstrj"
28
29# --- Step 2: Identify water-oxygen atoms ---
30wat_find = LammpsDumpWaterFinder(
31 filename,
32 oxygen_type=1,
33 hydrogen_type=2,
34)
35oxygen_indices = wat_find.get_water_oxygen_indices(frame_index=0)
36print("Number of water molecules:", len(oxygen_indices))
37
38# --- Step 3: Define the grid ---
39grid_params = {
40 "xi_0": 0.0,
41 "xi_f": 70.0,
42 "dx": 2.0,
43 "zi_0": 0.0,
44 "zi_f": 70.0,
45 "dz": 2.0,
46}
47
48# --- Step 4: Pick a density estimator ---
49# Top-hat histogram on the sampling grid (default):
50estimator = DensityEstimator.binning()
51# Swap in the Gaussian KDE for smoother per-cell density. ``density_sigma``
52# is the Gaussian kernel width; 3 Å is a sensible default for
53# room-temperature water:
54# estimator = DensityEstimator.gaussian(density_sigma=2.5)
55
56# --- Step 5: Build the analyzer ---
57analyzer = CoupledFit2DAnalyzer(
58 parser=LammpsDumpParser(filename),
59 atom_indices=oxygen_indices,
60 droplet_geometry="spherical",
61 grid_params=grid_params,
62 density_estimator=estimator,
63 # Pool 10 frames per batch — the coupled fit benefits from
64 # statistics; ``batch_size=-1`` pools the entire trajectory.
65 temporal_aggregator=TemporalAggregator(batch_size=10),
66)
67
68# --- Step 6: Run analysis on a frame range ---
69# 20 frames at batch_size=10 gives two pooled batches.
70results = analyzer.analyze(range(0, 20))
71print("Mean contact angle (°):", results.mean_angle)
72print("Std across batches (°):", results.std_angle)
73
74# Per-batch detail:
75batch = results.batches[0]
76print(
77 f"Frames {batch.frames[0]}–{batch.frames[-1]}: "
78 f"angle = {batch.angle:.2f}°, "
79 f"R_eq = {batch.model_params['R_eq']:.2f} Å, "
80 f"z_wall = {batch.model_params['zi_0']:.2f} Å"
81)
Coupled-Fit 3D Contact Angle
Full 3D coupled hyperbolic-tangent fit via
CoupledFit3DAnalyzer — nine-parameter model on a
(xi, yi, zi) density grid. Spherical droplets only.
1"""Coupled-fit 3D contact-angle example.
2
3Runs the 3D coupled hyperbolic-tangent fit on a full ``(xi, yi, zi)``
4density grid via :class:`CoupledFit3DAnalyzer`. The nine-parameter
5model fits the spherical-cap interface and the wall plane
6simultaneously, recovering a single robust angle per pooled batch.
7
8Only spherical droplets are supported — cylindrical droplets reduce to
9the 2D coupled fit by translational symmetry.
10"""
11
12from wetting_angle_kit.analysis import (
13 CoupledFit3DAnalyzer,
14 DensityEstimator,
15)
16from wetting_angle_kit.analysis.temporal import TemporalAggregator
17from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder
18
19# --- Step 1: Define the trajectory file ---
20filename = "../../tests/trajectories/traj_spherical_drop_4k.lammpstrj"
21
22# --- Step 2: Identify water-oxygen atoms ---
23wat_find = LammpsDumpWaterFinder(
24 filename,
25 oxygen_type=1,
26 hydrogen_type=2,
27)
28oxygen_indices = wat_find.get_water_oxygen_indices(frame_index=0)
29print("Number of water molecules:", len(oxygen_indices))
30
31# --- Step 3: Define the 3D grid ---
32# xi/yi are in the droplet-centred frame; zi is in the lab frame so
33# the wall position retains physical meaning.
34grid_params = {
35 "xi_0": -30.0,
36 "xi_f": 30.0,
37 "dx": 3.2,
38 "yi_0": -30.0,
39 "yi_f": 30.0,
40 "dy": 3.2,
41 "zi_0": 0.0,
42 "zi_f": 60.0,
43 "dz": 4.0,
44}
45
46# --- Step 4: Pick a density estimator ---
47# Top-hat histogram on the 3D sampling grid (default):
48estimator = DensityEstimator.binning()
49# Swap in the Gaussian KDE for smoother per-cell density:
50# estimator = DensityEstimator.gaussian(density_sigma=3.0)
51
52# --- Step 5: Build the analyzer ---
53analyzer = CoupledFit3DAnalyzer(
54 parser=LammpsDumpParser(filename),
55 atom_indices=oxygen_indices,
56 droplet_geometry="spherical",
57 grid_params=grid_params,
58 density_estimator=estimator,
59 # Pool all frames into a single batch — the 3D density needs more
60 # atoms than the 2D one for comparable per-cell noise.
61 temporal_aggregator=TemporalAggregator(batch_size=-1),
62)
63
64# --- Step 6: Run analysis ---
65n_frames = LammpsDumpParser(filename).frame_count()
66results = analyzer.analyze(range(0, n_frames))
67print("Mean contact angle (°):", results.mean_angle)
68
69# Per-batch detail:
70batch = results.batches[0]
71print(
72 f"Frames {batch.frames[0]}–{batch.frames[-1]}: "
73 f"angle = {batch.angle:.2f}°, "
74 f"R_eq = {batch.model_params['R_eq']:.2f} Å, "
75 f"z_wall = {batch.model_params['zi_0']:.2f} Å"
76)
77print(
78 f"Droplet centre: "
79 f"xi_c = {batch.model_params['xi_c']:.2f} Å, "
80 f"yi_c = {batch.model_params['yi_c']:.2f} Å, "
81 f"zi_c = {batch.model_params['zi_c']:.2f} Å"
82)
Visualising a Per-Frame Droplet Snapshot
Pull a single slice’s interface contour off a slicing-pipeline result
and render it with DropletSlicePlotter.
1"""End-to-end example: slicing pipeline + per-frame droplet snapshot.
2
3Runs the slicing-fit pipeline on a LAMMPS dump file, pulls one slice's
4interface contour + fitted circle off the result, and renders the
5droplet snapshot with :class:`DropletSlicePlotter`.
6"""
7
8from wetting_angle_kit.analysis import (
9 DensityEstimator,
10 InterfaceExtractor,
11 SpaceSampling,
12 SurfaceFitter,
13 TrajectoryAnalyzer,
14 WallDetector,
15)
16from wetting_angle_kit.analysis.temporal import TemporalAggregator
17from wetting_angle_kit.parsers import (
18 LammpsDumpParser,
19 LammpsDumpWallParser,
20 LammpsDumpWaterFinder,
21)
22from wetting_angle_kit.visualization import DropletSlicePlotter
23
24# --- 1. Define the input trajectory ---
25filename = "../../tests/trajectories/traj_10_3_330w_nve_4k_reajust.lammpstrj"
26frame_index = 10
27
28# --- 2. Identify water-oxygen atoms ---
29wat_find = LammpsDumpWaterFinder(filename, oxygen_type=1, hydrogen_type=2)
30oxygen_indices = wat_find.get_water_oxygen_indices(frame_index=0)
31print("Number of water molecules detected:", len(oxygen_indices))
32
33# --- 3. Read atom and wall positions for the frame ---
34parser = LammpsDumpParser(filepath=filename)
35oxygen_position = parser.parse(frame_index=frame_index, indices=oxygen_indices)
36
37# Wall parser: ``liquid_particle_types`` lists what to EXCLUDE
38# (the liquid), leaving the wall atoms.
39wall_parser = LammpsDumpWallParser(filename, liquid_particle_types=[1, 2])
40wall_coords = wall_parser.parse(frame_index=frame_index)
41
42# --- 4. Run the slicing pipeline on the chosen frame ---
43analyzer = TrajectoryAnalyzer(
44 parser=LammpsDumpParser(filename),
45 atom_indices=oxygen_indices,
46 droplet_geometry="cylinder_y",
47 interface_extractor=InterfaceExtractor(
48 sampling=SpaceSampling.rays(delta_cylinder=5.0, delta_polar=8.0),
49 density=DensityEstimator.gaussian(),
50 ),
51 surface_fitter=SurfaceFitter.slicing(surface_filter_offset=2.0),
52 wall_detector=WallDetector.min_plus_offset(offset=0.0),
53 temporal_aggregator=TemporalAggregator(batch_size=1),
54)
55batch = analyzer.analyze([frame_index]).batches[0]
56print("Per-slice contact angles (°):", batch.per_slice_angles.tolist())
57
58# --- 5. Visualise one slice ---
59plotter = DropletSlicePlotter(center=True)
60slice_idx = 0 # any 0..len(slice_surfaces)-1
61
62fig = plotter.plot_surface_points(
63 oxygen_position=oxygen_position,
64 surface_data=[batch.slice_surfaces[slice_idx]],
65 popt=batch.slice_popts[slice_idx],
66 wall_coords=wall_coords,
67 alpha=float(batch.per_slice_angles[slice_idx]),
68)
69
70fig.write_html("droplet_plot.html")
71print("Plot saved as 'droplet_plot.html'")
Angle Evolution + Density Contour Plots
The two trajectory-level plotters
(AngleEvolutionPlotter and DensityContourPlotter)
on the same trajectory.
1"""End-to-end example: angle evolution + density contour plots.
2
3Runs both the per-frame slicing pipeline and the coupled-fit
4analyzer on the same trajectory, then renders the two trajectory-level
5plots: the angle evolution curve (with per-batch ±σ band and running
6mean) and the density contour with the fitted spherical cap overlaid.
7
8The coupled-fit analyzer is built with the default histogram
9estimator; pass ``density_estimator=DensityEstimator.gaussian(...)``
10to render the contour over a smoothed density field instead.
11"""
12
13from wetting_angle_kit.analysis import (
14 CoupledFit2DAnalyzer,
15 DensityEstimator,
16 InterfaceExtractor,
17 SpaceSampling,
18 SurfaceFitter,
19 TrajectoryAnalyzer,
20 WallDetector,
21)
22from wetting_angle_kit.analysis.temporal import TemporalAggregator
23from wetting_angle_kit.parsers import LammpsDumpParser, LammpsDumpWaterFinder
24from wetting_angle_kit.visualization import (
25 AngleEvolutionPlotter,
26 DensityContourPlotter,
27)
28
29filename = "../../tests/trajectories/traj_spherical_drop_4k.lammpstrj"
30
31# Water-oxygen atoms.
32wat_find = LammpsDumpWaterFinder(filename, oxygen_type=1, hydrogen_type=2)
33oxygen_indices = wat_find.get_water_oxygen_indices(frame_index=0)
34
35# --- 1. Slicing pipeline → angle evolution figure ---
36slicing = TrajectoryAnalyzer(
37 parser=LammpsDumpParser(filename),
38 atom_indices=oxygen_indices,
39 droplet_geometry="spherical",
40 interface_extractor=InterfaceExtractor(
41 sampling=SpaceSampling.rays(delta_azimuthal=20.0, delta_polar=8.0),
42 density=DensityEstimator.gaussian(),
43 ),
44 surface_fitter=SurfaceFitter.slicing(surface_filter_offset=2.0),
45 wall_detector=WallDetector.min_plus_offset(offset=0.0),
46 temporal_aggregator=TemporalAggregator(batch_size=1),
47)
48slicing_results = slicing.analyze(range(0, 24))
49
50splot = AngleEvolutionPlotter(
51 slicing_results,
52 label="spherical_4k",
53 timestep=0.5,
54 time_unit="ps",
55)
56fig_evolution = splot.plot(per_frame_std=True, running_mean=True)
57fig_evolution.write_html("angle_evolution.html")
58print("Saved angle_evolution.html")
59
60# --- 2. Coupled-fit analyzer → density contour figure ---
61coupled_fit = CoupledFit2DAnalyzer(
62 parser=LammpsDumpParser(filename),
63 atom_indices=oxygen_indices,
64 droplet_geometry="spherical",
65 grid_params={
66 "xi_0": 0.0,
67 "xi_f": 70.0,
68 "dx": 2.0,
69 "zi_0": 0.0,
70 "zi_f": 70.0,
71 "dz": 2.0,
72 },
73 # density_estimator=DensityEstimator.gaussian(density_sigma=2.5),
74 temporal_aggregator=TemporalAggregator(batch_size=10),
75)
76coupled_fit_results = coupled_fit.analyze(range(0, 100))
77
78# Pick the first batch (or pass ``coupled_fit_results`` directly to
79# average the density across all batches before contouring).
80bplot = DensityContourPlotter(coupled_fit_results.batches[0], label="spherical_4k")
81fig_density = bplot.plot()
82fig_density.write_html("density_contour.html")
83print("Saved density_contour.html")