"""Fixed narrow openings, shorter wavelength, then an explicit intensity view. Each frame is a monochromatic steady state of the same two finite apertures. The scalar forward angular spectrum includes evanescent decay. The axial carrier is factored out for accurate sampling of the envelope at tiny lambda; it is restored whenever the real part is drawn. This is not a moving-frequency transient. Reflection, polarization, and mask thickness are outside the model. The labeled transition to |U|^2 is a change of displayed observable, not an average of the red/blue colors and not a replacement of diffraction by rays. """ from __future__ import annotations import argparse import json from pathlib import Path import subprocess import time import generate_symmetry_widening_apertures as style np,Image,ImageDraw=style.np,style.Image,style.ImageDraw ROOT=Path(__file__).resolve().parents[1] OUT=ROOT/'content'/'drafts'/'animations' NAME='symmetry-short-wave-beams' WIDTH,HEIGHT=1440,1120 BOX=style.BOX PW,PH=style.PW,style.PH XMIN,XMAX,YHALF=-3.,10.,4.8 CENTERS=(-2.4,2.4) OPENING=.5 PERIOD,N=128.,16384 NX=301 FPS,DURATION=30,20. FRAMES=round(FPS*DURATION) LAMBDAS=(.5,.2,.08,.015,.001) TRANSITIONS=((2.,5.),(6.,9.),(10.,13.),(14.,17.)) SAMPLES=(.8,5.5,8.4,11.5,15.4,18.5) BG,INK,MUTED=style.BG,style.INK,style.MUTED INTENSITY_COLOR=(255,181,77) PHASE_ANCHOR=(XMIN+XMAX)/2 FADE_BEGIN,FADE_END=.30,.18 font=style.font def wavelength_at(seconds): previous=LAMBDAS[0] for (begin,end),target in zip(TRANSITIONS,LAMBDAS[1:]): if seconds0) py=(self.y-self.ygrid[0])/self.dy self.yi=np.floor(py).astype(int) self.yf=(py-self.yi).astype(np.float32)[:,None] px=self.x[self.down]/XMAX*(nx-1) self.xi=np.minimum(np.floor(px).astype(int),nx-2) self.xf=(px-self.xi).astype(np.float32)[None,:] self.last_lambda,self.last_envelope=None,None def envelope(self,wavelength): if wavelength==self.last_lambda: return self.last_envelope k=2*np.pi/wavelength root=np.sqrt((k*k-self.q*self.q).astype(complex)) # Algebraically root-k, without cancellation for k >> |q|. delta=-self.q*self.q/(root+k) crop=np.empty((PH,self.nx),np.complex64) for begin in range(0,self.nx,24): end=min(begin+24,self.nx) h=np.exp(1j*self.z[begin:end,None]*delta[None,:]) u=np.fft.fftshift(np.fft.ifft(h*self.spectrum,axis=1),axes=1).T crop[:,begin:end]=(1-self.yf)*u[self.yi,:]+self.yf*u[self.yi+1,:] downstream=(1-self.xf)*crop[:,self.xi]+self.xf*crop[:,self.xi+1] self.last_lambda,self.last_envelope=wavelength,downstream return downstream def intensity_rgb(values): # One fixed intensity map for incoming and outgoing |U|^2. Low diffracted # intensity is plum/red, beam cores gold, and Fresnel peaks pale yellow. # No normalization by stage, by beam, or by local maximum. levels=(0.,.025,.1,.25,.5,1.,1.7,2.5) colors=np.array((BG,(15,7,20),(63,15,51),(146,30,58), (241,79,36),(255,186,81),(255,239,165),(255,252,222))) return np.rint(np.stack([np.interp(values,levels,colors[:,c]) for c in range(3)],axis=-1)).astype(np.uint8) def frame(seconds,sampler,clock_seconds=None): wavelength=wavelength_at(seconds) mix=intensity_mix(wavelength) env=sampler.envelope(wavelength) intensity=np.ones((PH,PW),np.float32) intensity[:,sampler.down]=abs(env)**2 rgb=intensity_rgb(intensity) if mix<1: clock=seconds if clock_seconds is None else clock_seconds # Playback clock is chosen for legibility, not a physical frequency # sweep. The spatial wavelength is always the stated value. carrier=np.exp(1j*(2*np.pi/wavelength*(sampler.x-PHASE_ANCHOR)-2*np.pi*clock/1.2)) real=np.broadcast_to(carrier.real,(PH,PW)).copy() real[:,sampler.down]=(env*carrier[None,sampler.down]).real indices=np.rint((np.clip(real,-1,1)+1)*4096).astype(np.int32) waves=style.color_table()[indices] rgb=np.rint((1-mix)*waves+mix*rgb).astype(np.uint8) picture=Image.new('RGB',(WIDTH,HEIGHT),BG) picture.paste(Image.fromarray(rgb[::-1]),BOX[:2]) d=ImageDraw.Draw(picture,'RGBA') sx=style.map_xy(0,0)[0] segments=((-YHALF,CENTERS[0]-OPENING/2), (CENTERS[0]+OPENING/2,CENTERS[1]-OPENING/2), (CENTERS[1]+OPENING/2,YHALF)) for bottom,top in segments: d.line((sx,style.map_xy(0,bottom)[1],sx,style.map_xy(0,top)[1]), fill=(*INK,255),width=6) d.text((70,28),'Shortening the wavelength',font=font(32,True),fill=INK) d.text((1370,31),'Same two openings',font=font(22),fill=MUTED,anchor='ra') if mix==0: d.text((70,1070),'Real part of the wave',font=font(20),fill=MUTED) for x,s,color in ((283,'−',style.NEGATIVE),(319,'0',MUTED),(356,'+',style.POSITIVE)): d.text((x,1070),s,font=font(21),fill=color) elif mix<1: d.text((70,1070),'Wave crests → intensity',font=font(20),fill=INK) else: d.text((70,1070),'Intensity',font=font(20),fill=INTENSITY_COLOR) d.text((180,1070),'Averaged over a wave cycle',font=font(20),fill=MUTED) ratio=OPENING/wavelength d.text((1370,1070),f'Opening width: {ratio:.1f} wavelengths', font=font(22),fill=INK,anchor='ra') return picture def previews(sampler): OUT.mkdir(parents=True,exist_ok=True) sheet=Image.new('RGB',(1440,3*596),BG) for j,seconds in enumerate(SAMPLES): pic=frame(seconds,sampler) pic.save(OUT/f'{NAME}-check-{seconds:g}.png') x,y=(j%2)*720,(j//2)*596 sheet.paste(pic.resize((720,560),Image.Resampling.LANCZOS),(x,y)) ImageDraw.Draw(sheet).text((x+35,y+568),f'{seconds:g} s',fill=MUTED,font=font(18)) sheet.save(OUT/f'{NAME}-contact-sheet.png') frame(18.5,sampler).save(OUT/f'{NAME}-poster.png') print(str(OUT/f'{NAME}-contact-sheet.png'),flush=True) def validate(sampler): errors=[] for wavelength in (LAMBDAS[0],.08,LAMBDAS[-1]): sampled=sampler.envelope(wavelength) k=2*np.pi/wavelength root=np.sqrt((k*k-sampler.q*sampler.q).astype(complex)) for requested in (.2,3.25,9.8): col=int(np.argmin(abs(sampler.x[sampler.down]-requested))) z=sampler.x[sampler.down[col]] reference=np.fft.fftshift(np.fft.ifft(sampler.spectrum*np.exp(1j*z*(root-k)))) reference=np.interp(sampler.y,sampler.ygrid,reference) errors.append(float(np.max(abs(sampled[:,col]-reference)))) assert max(errors)<.02,errors assert np.all(np.diff([wavelength_at(t) for t in np.linspace(0,DURATION,601)])<=0) assert intensity_mix(LAMBDAS[-1])==1 and intensity_mix(LAMBDAS[0])==0 steps=[] for i in range(FRAMES-1): wa,wb=wavelength_at(i/FPS),wavelength_at((i+1)/FPS) if intensity_mix(wa)==1: continue phase_step=(2*np.pi/wb-2*np.pi/wa)*(np.array((XMIN,XMAX))-PHASE_ANCHOR)-2*np.pi/(1.2*FPS) steps.append(float(max(abs(phase_step)))) assert max(steps)