Analysis Tools¶
This guide covers the analysis tools available in Cavity HOOMD for interpreting simulation results.
Energy Tracking and Interpretation¶
Energy Components¶
Cavity HOOMD tracks multiple energy components:
Energy Decomposition:
Where:
\(E_{\text{kinetic}}\): Molecular kinetic energy
\(E_{\text{potential}}\): Molecular potential energy (LJ, bonds, etc.)
\(E_{\text{cavity}} = \frac{1}{2}K(q_x^2 + q_y^2)\): Cavity harmonic energy
\(E_{\text{coupling}} = g(q_x D_x + q_y D_y)\): Dipole-field coupling
\(E_{\text{self}} = \frac{g^2}{2K}(D_x^2 + D_y^2)\): Dipole self-energy
Reading Energy Files¶
Energy tracker output format:
import numpy as np
import matplotlib.pyplot as plt
# Load energy data
data = np.loadtxt('prod-1_energy_tracker.txt', skiprows=1, delimiter='\t')
# Extract columns
time_ps = data[:, 0]
E_kinetic = data[:, 1]
E_potential = data[:, 2]
E_cavity = data[:, 3]
E_coupling = data[:, 4]
E_self = data[:, 5]
E_total = data[:, 6]
# Plot energy components
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))
# Total energy (conservation check)
ax1.plot(time_ps, E_total, 'k-', linewidth=2)
ax1.set_ylabel('Total Energy')
ax1.set_title('Energy Conservation')
ax1.grid(True, alpha=0.3)
# Energy components
ax2.plot(time_ps, E_kinetic, label='Kinetic')
ax2.plot(time_ps, E_potential, label='Potential')
ax2.plot(time_ps, E_cavity, label='Cavity')
ax2.plot(time_ps, E_coupling, label='Coupling')
ax2.plot(time_ps, E_self, label='Self-energy')
ax2.set_xlabel('Time (ps)')
ax2.set_ylabel('Energy')
ax2.set_title('Energy Components')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('energy_analysis.png', dpi=300)
plt.show()
Interpreting Energy Behavior¶
Normal Behavior:
Total energy conservation:
In NVE or during unperturbed NVT:
\[\frac{\Delta E_{\text{total}}}{E_{\text{total}}} < 0.01\%\]Kinetic-potential exchange:
Kinetic and potential energies oscillate in anti-phase:
correlation = np.corrcoef(E_kinetic, E_potential)[0, 1] print(f"KE-PE correlation: {correlation:.3f}") # Should be negative
Coupling energy:
Magnitude: \(|E_{\text{coupling}}| \sim g \sqrt{N}\)
Sign: Can be positive or negative
Fluctuations: Indicates molecular-cavity energy exchange
Warning Signs:
Energy drift:
drift_percent = (E_total[-1] - E_total[0]) / E_total[0] * 100 if abs(drift_percent) > 0.1: print(f"WARNING: Energy drift {drift_percent:.2f}%")
Causes: Timestep too large, numerical instability, bad initial configuration.
Energy spikes:
Sudden jumps indicate particle overlaps or force discontinuities.
Unbounded growth:
Cavity energy continuously increasing suggests missing dissipation.
Cavity Mode Trajectory Analysis¶
Loading Cavity Mode Data¶
Cavity mode file format:
import numpy as np
import matplotlib.pyplot as plt
# Load cavity mode data
data = np.loadtxt('prod-1_cavity_mode.txt', skiprows=1, delimiter='\t')
time_ps = data[:, 0]
q_x = data[:, 1] # X-polarization position
q_y = data[:, 2] # Y-polarization position
v_x = data[:, 3] # X-polarization velocity
v_y = data[:, 4] # Y-polarization velocity
# Calculate total amplitude
q_total = np.sqrt(q_x**2 + q_y**2)
v_total = np.sqrt(v_x**2 + v_y**2)
Cavity Mode Oscillations¶
Analyzing oscillation frequency:
from scipy import signal
# FFT analysis
sampling_rate = 1.0 / (time_ps[1] - time_ps[0]) # Hz
frequencies, power = signal.welch(q_x, fs=sampling_rate, nperseg=1024)
# Find peak frequency
peak_idx = np.argmax(power)
peak_freq_Hz = frequencies[peak_idx]
peak_freq_cm = peak_freq_Hz * 33.356 # Convert to cm⁻¹
print(f"Cavity oscillation frequency: {peak_freq_cm:.1f} cm⁻¹")
# Plot power spectrum
plt.figure(figsize=(10, 5))
plt.semilogy(frequencies * 33.356, power)
plt.xlabel('Frequency (cm⁻¹)')
plt.ylabel('Power Spectral Density')
plt.title('Cavity Mode Spectrum')
plt.xlim(0, 5000)
plt.grid(True, alpha=0.3)
plt.show()
Phase Space Trajectory:
# Plot phase space (position vs velocity)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
# X-polarization
ax1.plot(q_x, v_x, alpha=0.5, linewidth=0.5)
ax1.set_xlabel('Position q_x')
ax1.set_ylabel('Velocity v_x')
ax1.set_title('X-Polarization Phase Space')
ax1.grid(True, alpha=0.3)
# Y-polarization
ax2.plot(q_y, v_y, alpha=0.5, linewidth=0.5)
ax2.set_xlabel('Position q_y')
ax2.set_ylabel('Velocity v_y')
ax2.set_title('Y-Polarization Phase Space')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Cavity-Molecule Energy Exchange¶
Correlation analysis:
# Calculate cavity energy
K = 1.0 # Cavity spring constant (depends on frequency and mass)
E_cavity = 0.5 * K * (q_x**2 + q_y**2)
# Load molecular kinetic energy from energy tracker
E_kinetic = np.loadtxt('prod-1_energy_tracker.txt',
skiprows=1, delimiter='\t', usecols=1)
# Cross-correlation
correlation = np.correlate(E_cavity - np.mean(E_cavity),
E_kinetic - np.mean(E_kinetic),
mode='same')
lags = np.arange(-len(correlation)//2, len(correlation)//2)
lag_times = lags * (time_ps[1] - time_ps[0])
plt.figure(figsize=(10, 5))
plt.plot(lag_times, correlation / np.max(correlation))
plt.xlabel('Lag Time (ps)')
plt.ylabel('Normalized Cross-Correlation')
plt.title('Cavity-Molecular Energy Exchange')
plt.grid(True, alpha=0.3)
plt.show()
Temperature Decomposition¶
For diatomic molecules, temperature can be decomposed into translational, rotational, and vibrational components.
Using DiatomicMolecularTemperatures¶
Setup in simulation:
from cavitymd.molecular_temperatures import DiatomicMolecularTemperatures
from cavitymd.analysis import ElapsedTimeTracker
# Create trackers
time_tracker = ElapsedTimeTracker(simulation=sim, runtime_ps=1000.0)
temp_tracker = DiatomicMolecularTemperatures(
simulation=sim,
time_tracker=time_tracker,
output_period_ps=1.0,
output_file='molecular_temps.csv'
)
# Add to simulation
sim.operations.writers.append(time_tracker)
sim.operations.writers.append(temp_tracker)
Analyzing output:
import pandas as pd
# Load temperature data
df = pd.read_csv('molecular_temps.csv')
# Extract columns
time = df['time_ps']
T_trans = df['T_translational_K']
T_rot = df['T_rotational_K']
T_vib = df['T_vibrational_K']
T_total = df['T_total_K']
# Plot
fig, ax = plt.subplots(figsize=(12, 6))
ax.plot(time, T_trans, label='Translational', linewidth=2)
ax.plot(time, T_rot, label='Rotational', linewidth=2)
ax.plot(time, T_vib, label='Vibrational', linewidth=2)
ax.plot(time, T_total, label='Total (Kinetic)',
linestyle='--', color='black', linewidth=2)
ax.axhline(100.0, color='red', linestyle=':',
label='Target Temperature')
ax.set_xlabel('Time (ps)', fontsize=12)
ax.set_ylabel('Temperature (K)', fontsize=12)
ax.set_title('Molecular Temperature Decomposition', fontsize=14)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('temperature_decomposition.png', dpi=300)
plt.show()
Equipartition Check¶
Theory: At equilibrium, each quadratic degree of freedom should have energy \(\frac{1}{2}k_B T\):
Checking equipartition:
# Calculate average temperatures (after equilibration)
equilibration_time = 100.0 # ps
eq_mask = time > equilibration_time
T_trans_avg = T_trans[eq_mask].mean()
T_rot_avg = T_rot[eq_mask].mean()
T_vib_avg = T_vib[eq_mask].mean()
T_target = 100.0
print(f"Target temperature: {T_target:.1f} K")
print(f"Translational: {T_trans_avg:.1f} K ({T_trans_avg/T_target*100:.1f}%)")
print(f"Rotational: {T_rot_avg:.1f} K ({T_rot_avg/T_target*100:.1f}%)")
print(f"Vibrational: {T_vib_avg:.1f} K ({T_vib_avg/T_target*100:.1f}%)")
# Check equipartition (should all be close to target)
deviation_trans = abs(T_trans_avg - T_target) / T_target * 100
deviation_rot = abs(T_rot_avg - T_target) / T_target * 100
deviation_vib = abs(T_vib_avg - T_target) / T_target * 100
if max(deviation_trans, deviation_rot, deviation_vib) < 5.0:
print("✓ Equipartition satisfied (within 5%)")
else:
print("✗ Equipartition violation detected")
Correlation Functions and IR Spectra¶
Dipole Autocorrelation Function¶
Computing autocorrelation:
import gsd.hoomd
# Load trajectory
traj = gsd.hoomd.open('prod-1.gsd')
# Extract dipole moments
dipole_moments = []
for frame in traj:
# Calculate total dipole (simplified for O2)
positions = frame.particles.position[:-2] # Exclude cavity particles
types = frame.particles.typeid[:-2]
# For diatomic O2, dipole ~ separation vector
N_molecules = len(positions) // 2
dipoles = []
for i in range(N_molecules):
r1 = positions[2*i]
r2 = positions[2*i + 1]
dipole = r2 - r1
dipoles.append(dipole)
total_dipole = np.sum(dipoles, axis=0)
dipole_moments.append(total_dipole)
dipole_moments = np.array(dipole_moments)
# Compute autocorrelation
def autocorr(x):
result = np.correlate(x, x, mode='full')
return result[len(result)//2:] / result[len(result)//2]
# Autocorrelation for each component
C_x = autocorr(dipole_moments[:, 0])
C_y = autocorr(dipole_moments[:, 1])
C_z = autocorr(dipole_moments[:, 2])
# Average
C_avg = (C_x + C_y + C_z) / 3.0
# Time axis
dt = 0.01 # ps (output frequency)
times = np.arange(len(C_avg)) * dt
# Plot
plt.figure(figsize=(10, 5))
plt.plot(times[:1000], C_avg[:1000]) # First 10 ps
plt.xlabel('Time (ps)')
plt.ylabel('Autocorrelation C(t)')
plt.title('Dipole Moment Autocorrelation')
plt.grid(True, alpha=0.3)
plt.show()
IR Spectrum Calculation¶
Fourier transform of autocorrelation:
from scipy import signal
# Window function to reduce ringing
window = signal.windows.blackman(len(C_avg))
C_windowed = C_avg * window
# FFT
frequencies = np.fft.rfftfreq(len(C_windowed), d=dt)
spectrum = np.fft.rfft(C_windowed)
intensity = np.abs(spectrum)**2
# Convert frequency to cm⁻¹
freq_cm = frequencies * 33.356 # Conversion factor
# Plot IR spectrum
plt.figure(figsize=(12, 5))
plt.plot(freq_cm, intensity)
plt.xlabel('Frequency (cm⁻¹)')
plt.ylabel('Intensity (arbitrary units)')
plt.title('Infrared Absorption Spectrum')
plt.xlim(0, 3000)
plt.grid(True, alpha=0.3)
plt.savefig('ir_spectrum.png', dpi=300)
plt.show()
Identifying peaks:
# Find peaks in spectrum
peaks, properties = signal.find_peaks(intensity,
height=np.max(intensity)*0.1,
prominence=np.max(intensity)*0.05)
print("Vibrational frequencies:")
for i, peak_idx in enumerate(peaks):
freq = freq_cm[peak_idx]
height = intensity[peak_idx]
print(f" Peak {i+1}: {freq:.1f} cm⁻¹ (intensity: {height:.2e})")
Plotting Utilities and Visualization¶
Trajectory Visualization¶
Using OVITO (external tool):
ovito prod-1.gsd
Using MDAnalysis (Python):
import MDAnalysis as mda
from MDAnalysis.analysis import rdf
# Load trajectory
u = mda.Universe('init-0.gsd', 'prod-1.gsd')
# Select atoms
oxygens = u.select_atoms('type O')
# Calculate radial distribution function
rdf_analysis = rdf.InterRDF(oxygens, oxygens,
nbins=100, range=(0.0, 10.0))
rdf_analysis.run()
# Plot
plt.figure(figsize=(10, 5))
plt.plot(rdf_analysis.results.bins, rdf_analysis.results.rdf)
plt.xlabel('Distance (Å)')
plt.ylabel('g(r)')
plt.title('Radial Distribution Function')
plt.grid(True, alpha=0.3)
plt.show()
Creating Publication-Quality Figures¶
Template for high-quality plots:
import matplotlib.pyplot as plt
import matplotlib as mpl
# Set publication style
mpl.rcParams['font.size'] = 12
mpl.rcParams['font.family'] = 'sans-serif'
mpl.rcParams['axes.linewidth'] = 1.5
mpl.rcParams['xtick.major.width'] = 1.5
mpl.rcParams['ytick.major.width'] = 1.5
mpl.rcParams['xtick.major.size'] = 5
mpl.rcParams['ytick.major.size'] = 5
fig, ax = plt.subplots(figsize=(6, 4))
# Plot data
ax.plot(x_data, y_data, 'o-', linewidth=2, markersize=4,
label='Cavity-coupled')
ax.plot(x_control, y_control, 's--', linewidth=2, markersize=4,
label='Control')
# Labels and formatting
ax.set_xlabel('Time (ps)', fontsize=14)
ax.set_ylabel('Observable', fontsize=14)
ax.legend(frameon=True, fontsize=12, loc='best')
ax.grid(True, alpha=0.3, linestyle='--')
# Tight layout
plt.tight_layout()
# Save
plt.savefig('figure.pdf', dpi=300, bbox_inches='tight')
plt.savefig('figure.png', dpi=300, bbox_inches='tight')
plt.show()
Next Steps¶
You now know how to:
Track and interpret energy components
Analyze cavity mode trajectories
Decompose molecular temperatures
Calculate correlation functions and IR spectra
Create publication-quality visualizations
Continue to:
Time-Varying Coupling for time-dependent coupling analysis
FDR Temperature for advanced temperature measurements
Correlation Analysis for detailed correlation analysis
Energy Conservation for energy conservation theory