codexcontact-mode-phase-accumulation.py
contact-mode-phase-accumulation.py
"""Exact numerical frequency and accumulated phase of an undamped contact mode.
Python 3.14, NumPy and Matplotlib; see pyproject.toml. Writes its PNG to cwd.
"""
from pathlib import Path
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
def main():
z = np.linspace(0.001, 1.9, 800)
central = 2 * np.arcsin(z / 2) / z - 1
trapezoidal = 2 * np.arctan(z / 2) / z - 1
step = np.geomspace(1e-6, 1e-3, 600)
omega, duration = 1000.0, 0.1
phase_c = duration * (2 * np.arcsin(step * omega / 2) / step - omega)
phase_t = duration * (omega - 2 * np.arctan(step * omega / 2) / step)
fig, axes = plt.subplots(1, 2, figsize=(9, 4.25), dpi=120, layout='constrained')
fig.patch.set_facecolor('white')
axes[0].plot(z, 100 * central, label='Central difference', color='#176da5')
axes[0].plot(z, 100 * trapezoidal, label='Trapezoidal', color='#bf5726')
axes[0].axhline(0, color='#777777', linewidth=0.7)
axes[0].set(xlabel=r'Dimensionless step $h\omega$', ylabel='Numerical frequency error (%)',
title='Both curves are within their stable ranges')
axes[1].loglog(step * 1e6, phase_c, label='Central difference', color='#176da5')
axes[1].loglog(step * 1e6, phase_t, label='Trapezoidal', color='#bf5726')
axes[1].axhline(0.01, color='#555555', linestyle='--', label='0.01 rad phase target')
axes[1].set(xlabel='Step h (microseconds)', ylabel='Absolute accumulated phase error (rad)',
title='1000 rad/s mode over 0.1 s')
for axis in axes:
axis.grid(alpha=0.2, which='both')
axis.legend(fontsize=8)
fig.suptitle('Stable contact-mode integration can still drift in phase', fontsize=13)
fig.savefig(Path('contact-mode-phase-accumulation.png'), facecolor='white', transparent=False)
plt.close(fig)
if __name__ == '__main__':
main()