codexholography-off-axis-cross-term-separation.py
holography-off-axis-cross-term-separation.py
"""Original periodic off-axis hologram; Python 3.14 / NumPy 2.3.5 / Matplotlib 3.10.7.
Run from _media. Uses NumPy FFT only, with no propagation or SciPy. Checks
analytic Fourier coefficients, complex-field recovery and playback orders.
"""
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt
N = 1024
x = np.arange(N) / N
O = .2*np.exp(2j*np.pi*3*x) + .1*np.exp(-2j*np.pi*5*x)
R = np.exp(2j*np.pi*20*x)
I = np.abs(R + O)**2
analytic = 1.05 + .04*np.cos(2*np.pi*8*x) + .4*np.cos(2*np.pi*17*x) + .2*np.cos(2*np.pi*25*x)
np.testing.assert_allclose(I, analytic, atol=2e-14, rtol=0)
coeff = np.fft.fft(I)/N
freq = np.fft.fftfreq(N, 1/N)
expected = np.zeros(N, dtype=complex)
for m, a in [(0,1.05), (8,.02),(-8,.02),(17,.2),(-17,.2),(25,.1),(-25,.1)]:
expected[m % N] = a
np.testing.assert_allclose(coeff, expected, atol=2e-14, rtol=0)
mask = (freq >= -30) & (freq <= -12)
recovered = R*np.fft.ifft(N*coeff*mask)
np.testing.assert_allclose(recovered, O, atol=2e-14, rtol=0)
t0, beta = .3, .01
t = t0 + beta*I
playback = R*t
background = (t0+beta*(1+np.abs(O)**2))*R
desired = beta*O
conjugate = beta*R**2*O.conj()
np.testing.assert_allclose(playback, background+desired+conjugate, atol=2e-14, rtol=0)
assert t.min() >= .3049-1e-14 and t.max() <= .3169+1e-14
for term, modes in [(desired,{3:.002,-5:.001}), (conjugate,{37:.002,45:.001}), (background,{20:.3105,12:.0002,28:.0002})]:
target = np.zeros(N, dtype=complex)
for m,a in modes.items(): target[m % N] = a
np.testing.assert_allclose(np.fft.fft(term)/N, target, atol=2e-14, rtol=0)
error = float(np.max(np.abs(recovered-O)))
print(f'Max complex recovery error: {error:.3g}; all analytic and playback checks passed')
plt.rcParams.update({'font.size': 10, 'figure.facecolor': 'white', 'axes.facecolor': 'white'})
fig, axes = plt.subplots(2, 2, figsize=(10, 6.2))
fig.subplots_adjust(left=.08, right=.975, top=.86, bottom=.1, hspace=.49, wspace=.28)
fig.suptitle('A tilted reference separates the two encoded fields', fontsize=16)
ax=axes[0,0]
ax.plot(x,I,color='#333333',lw=1.3)
ax.set(title=r'Record $I=|R+O|^2$', xlabel='x (unit interval)',ylabel='Intensity (chosen units)',xlim=(0,1))
ax=axes[0,1]
colors={0:'.35',8:'.35',-8:'.35',-17:'#1967a3',-25:'#1967a3',17:'#ba5316',25:'#ba5316'}
for m in sorted(colors):
a=abs(coeff[m%N]); ax.vlines(m,0,a,color=colors[m],lw=2);ax.plot(m,a,'o',color=colors[m],ms=4)
ax.axvspan(-30,-12,color='#1967a3',alpha=.12,label='Retained band')
ax.set(title='Normalized FFT coefficients',xlabel='Spatial frequency (cycles / unit)',ylabel='Coefficient magnitude',xlim=(-33,33),ylim=(0,1.2))
ax.text(0,1.09,'Background',ha='center',fontsize=9)
ax.text(-21,.36,r'$R^*O$',ha='center',color='#1967a3')
ax.text(21,.36,r'$RO^*$',ha='center',color='#ba5316')
ax.legend(loc='upper left',fontsize=8)
for ax, part, label in [(axes[1,0],np.real,'Real'),(axes[1,1],np.imag,'Imaginary')]:
ax.plot(x,part(O),color='#1967a3',lw=2,label='Chosen O')
ax.plot(x[::16],part(recovered[::16]),'o',ms=3,mfc='white',mec='#ba5316',label='Recovered')
ax.set(title=f'Recovered {label.lower()} part of O',xlabel='x (unit interval)',ylabel='Complex amplitude',xlim=(0,1),ylim=(-.34,.34))
ax.legend(fontsize=8,loc='upper right')
for ax in axes.flat:
ax.spines[['top','right']].set_visible(False)
ax.grid(alpha=.16)
fig.text(.5,.015, 'Chosen periodic field; N = 1024. Exact integer bins avoid spectral leakage. No measured data.',ha='center',fontsize=10,color='.35')
fig.savefig(Path.cwd() / f'{Path(__file__).stem}.png',dpi=120,facecolor='white')
plt.close(fig)