Source code for wetting_angle_kit.visualization_angles.comparison_methods

import os

import matplotlib.pyplot as plt
import numpy as np


[docs] class MethodComparison: """Utility to compare statistics from multiple trajectory analyzers. Parameters ---------- analyzers : list Analyzer instances exposing ``directories`` and required API methods. method_names : list[str], optional Custom display names. If None, uses each analyzer's ``get_method_name``. """ def __init__(self, analyzers, method_names=None): self.analyzers = analyzers self.method_names = method_names or [a.get_method_name() for a in analyzers] for analyzer in self.analyzers: if not hasattr(analyzer, "data") or not analyzer.data: analyzer.load_data() def _check_and_run_analysis(self, analyzer): """Run analyzer if expected output file is absent for any directory. Parameters ---------- analyzer : BaseTrajectoryAnalyzer Analyzer instance whose output will be checked. """ for directory in analyzer.directories: output_file = f"{directory}/output_stats.txt" if not os.path.exists(output_file): raise FileNotFoundError( f"No analysis found for {directory}. " "Please run the analysis first." ) def _read_analysis_output(self, analyzer, directory): """Return mean surface area and angle parsed from stats file. Parameters ---------- analyzer : BaseTrajectoryAnalyzer Analyzer owning the directory. directory : str Path containing ``output_stats.txt``. Returns ------- tuple(float, float) (mean_surface_area, mean_contact_angle). """ output_file = f"{directory}/output_stats.txt" with open(output_file, "r") as f: lines = f.readlines() mean_surface_area = float(lines[2].split(": ")[1].strip()) mean_contact_angle = float(lines[3].split(": ")[1].strip().replace("°", "")) return mean_surface_area, mean_contact_angle
[docs] def plot_side_by_side_comparison( self, save_path=None, figsize=(14, 5), color="purple" ): """ Produce side-by-side comparison of mean contact angle vs. surface area scaling. Inspired by plot_mean_angle_vs_surface(). """ plt.rcParams.update( { "font.family": "serif", "font.size": 13, "axes.labelsize": 14, "axes.titlesize": 15, "legend.fontsize": 11, "xtick.direction": "in", "ytick.direction": "in", "axes.linewidth": 1.0, "errorbar.capsize": 3, } ) fig, axes = plt.subplots(1, len(self.analyzers), figsize=figsize) if len(self.analyzers) == 1: axes = [axes] for ax, analyzer, method_name in zip(axes, self.analyzers, self.method_names): # gather one point per directory xvals, yvals = [], [] for directory in analyzer.directories: mean_sa, mean_angle = self._read_analysis_output(analyzer, directory) x = 1.0 / np.sqrt(mean_sa) # same as example y = mean_angle ax.errorbar(x, y, yerr=0.5, fmt="o", color=color) ax.annotate( analyzer.get_clean_label(directory), xy=(x, y), xytext=(4, 4), textcoords="offset points", fontsize=7, ) xvals.append(x) yvals.append(y) # linear fit if we have ≥2 points xvals, yvals = np.array(xvals), np.array(yvals) if len(xvals) >= 2: coeffs = np.polyfit(xvals, yvals, 1) fit_line = np.poly1d(coeffs) x_fit = np.linspace(0, xvals.max() * 1.1, 100) ax.plot( x_fit, fit_line(x_fit), "--", color="gray", label=f"Fit: y={coeffs[0]:.2f}x+{coeffs[1]:.2f}", ) ax.set_xlabel(r"$1 / \sqrt{\text{Surface Area}}$") ax.set_ylabel("Mean Angle (°)") ax.set_title(method_name) ax.legend(frameon=False) ax.set_xlim(left=-0.001) if yvals.size > 0: ax.set_ylim(min(yvals) - 2, max(yvals) + 2) plt.tight_layout() if save_path: plt.savefig(save_path, dpi=400, bbox_inches="tight") plt.close()
[docs] def plot_overlay_comparison(self, save_path=None, figsize=(8, 6), color="purple"): """ Overlay mean angle vs surface area scaling across all analyzers. Inspired by plot_mean_angle_vs_surface(). """ plt.rcParams.update( { "font.family": "serif", "font.size": 13, "axes.labelsize": 14, "axes.titlesize": 15, "legend.fontsize": 10, "xtick.direction": "in", "ytick.direction": "in", "axes.linewidth": 1.0, "errorbar.capsize": 3, } ) fig, ax = plt.subplots(figsize=figsize) all_yvals = [] for analyzer, method_name in zip(self.analyzers, self.method_names): xvals, yvals = [], [] for directory in analyzer.directories: mean_sa, mean_angle = self._read_analysis_output(analyzer, directory) x = 1.0 / np.sqrt(mean_sa) y = mean_angle label = f"{method_name}{analyzer.get_clean_label(directory)}" ax.errorbar( x, y, yerr=0.5, fmt="o", color=color, alpha=0.7, label=label ) xvals.append(x) yvals.append(y) # Fit per method xvals, yvals = np.array(xvals), np.array(yvals) if len(xvals) >= 2: coeffs = np.polyfit(xvals, yvals, 1) fit_line = np.poly1d(coeffs) x_fit = np.linspace(0, xvals.max() * 1.1, 100) ax.plot( x_fit, fit_line(x_fit), "--", label=f"{method_name} fit: y={coeffs[0]:.2f}x+{coeffs[1]:.2f}", ) all_yvals.extend(yvals) ax.set_xlabel(r"$1 / \sqrt{\text{Surface Area}}$") ax.set_ylabel("Mean Angle (°)") ax.set_title("Method Comparison: Mean Angle vs Surface Area") ax.legend(frameon=False, fontsize=7) ax.set_xlim(left=-0.001) if all_yvals: ax.set_ylim(min(all_yvals) - 2, max(all_yvals) + 2) plt.tight_layout() if save_path: plt.savefig(save_path, dpi=400, bbox_inches="tight") plt.close()
[docs] def compare_statistics(self): """Print summary statistics aggregated across methods and directories.""" print("=" * 70) print("METHOD COMPARISON STATISTICS") print("=" * 70) for method_name, analyzer in zip(self.method_names, self.analyzers): print(f"\n{method_name}:") print("-" * 70) all_angles = [] all_surfaces = [] for directory in analyzer.directories: try: mean_surface_area, mean_contact_angle = self._read_analysis_output( analyzer, directory ) angles = analyzer.get_contact_angles(directory) surfaces = analyzer.get_surface_areas(directory) except FileNotFoundError: angles = analyzer.get_contact_angles(directory) surfaces = analyzer.get_surface_areas(directory) mean_surface_area = float(np.mean(surfaces)) mean_contact_angle = float(np.mean(angles)) all_angles.extend(angles) all_surfaces.extend(surfaces) print(f" {analyzer.get_clean_label(directory)}:") print(f" Mean Surface Area: {mean_surface_area:.4f}") print(f" Mean Angle: {mean_contact_angle:.4f}°") if all_angles: print("\n Overall Statistics:") print(f" Total samples: {len(all_angles)}") print(f" Mean Surface Area: {np.mean(all_surfaces):.4f}") print(f" Mean Angle: {np.mean(all_angles):.4f}°") print(f" Std Angle: {np.std(all_angles):.4f}°") print("\n" + "=" * 70)