"""Three increasing path lengths, two wavelengths, and accumulated phase. This standalone insert shows deterministic remainder-angle sensitivity. It does not use random stopping angles or claim three examples prove general interference cancellation. Existing path-diamond films remain unchanged. """ from __future__ import annotations import argparse import json import math import subprocess import time from functools import lru_cache from generate_symmetry_two_tip_to_tail_limits import ( np, Image, ImageDraw, imageio_ffmpeg, OUT, BG, PANEL, INK, MUTED, BORDER, BLUE, GOLD, AXIS, SUPERSAMPLE, pixels, text, line, arrow, ) NAME="symmetry-phase-remainder-spinners" WIDTH,HEIGHT,FPS=1440,870,60 PATH_LENGTHS=np.array([1.,2.,3.]) WAVELENGTHS=np.array([12.,5./47.]) TURNS=PATH_LENGTHS[:,None]/WAVELENGTHS[None,:] PHASES=2*np.pi*TURNS REMAINDERS=PHASES%(2*np.pi) PRELUDE,SPIN,HOLD=.8,3.6,1.8 PASS_SECONDS=SPIN+HOLD DURATION=PRELUDE+3*PASS_SECONDS TOTAL_FRAMES=round(DURATION*FPS) PRELUDE_FRAMES=round(PRELUDE*FPS) PASS_FRAMES=round(PASS_SECONDS*FPS) SPIN_FRAMES=round(SPIN*FPS) CENTERS=((367.,415.),(1073.,415.)) RADIUS=144. PANELS=((30,126,704,790),(736,126,1410,790)) def endpoint(center,angle,radius=RADIUS): return (center[0]+radius*math.cos(angle),center[1]-radius*math.sin(angle)) def circle(draw,center,radius,color,width=1): x,y=center draw.ellipse(pixels((x-radius,y-radius,x+radius,y+radius)), outline=color,width=max(1,round(width*SUPERSAMPLE))) def dot(draw,xy,color,radius=3): x,y=xy draw.ellipse(pixels((x-radius,y-radius,x+radius,y+radius)),fill=color) def arc(draw,center,angle,radius,color,width=2): if angle<=0: return angles=np.linspace(0,angle,max(2,int(angle*radius/2)+1)) points=[pixels(endpoint(center,a,radius)) for a in angles] draw.line(points,fill=color,width=round(width*SUPERSAMPLE)) def state(index): index=min(max(int(index),0),TOTAL_FRAMES-1) if index=SPIN_FRAMES def final_frame(stage): return PRELUDE_FRAMES+(stage+1)*PASS_FRAMES-1 def phase_at(stage,progress): # Each trial adds the same path length and continues from the prior stop. previous=PHASES[stage-1] if stage else np.zeros(2) return previous+(PHASES[stage]-previous)*progress @lru_cache(None) def background(): image=Image.new("RGB",(WIDTH*SUPERSAMPLE,HEIGHT*SUPERSAMPLE),BG) d=ImageDraw.Draw(image,"RGBA") text(d,(38,23),"Same paths · different wavelengths",31,bold=True) text(d,(39,68),"Three path lengths · the same change in L on both sides",19,MUTED) text(d,(1400,26),"φ = 2πL / λ",27,INK,anchor="ra") for pane,box in enumerate(PANELS): d.rounded_rectangle(pixels(box),radius=16*SUPERSAMPLE, fill=PANEL,outline=BORDER,width=SUPERSAMPLE) left,_,right,_=box title="Slower phase advance" if pane==0 else "Faster phase advance" subtitle="Longer wavelength · λ = 12" if pane==0 else "Shorter wavelength · λ = 5/47" text(d,(left+25,147),title,26,bold=True) text(d,(left+26,190),subtitle,19,MUTED) center=CENTERS[pane] line(d,(center[0]-RADIUS-19,center[1]),(center[0]+RADIUS+24,center[1]),AXIS,1) line(d,(center[0],center[1]-RADIUS-20),(center[0],center[1]+RADIUS+20),AXIS,1) circle(d,center,RADIUS,BORDER,1.1) text(d,(center[0]+RADIUS+34,center[1]-9),"Re",16,MUTED) text(d,(center[0]+8,center[1]-RADIUS-39),"Im",16,MUTED) for deg in range(0,360,30): angle=math.radians(deg) line(d,endpoint(center,angle,RADIUS-4),endpoint(center,angle,RADIUS+4),BORDER,1) if pane==0: text(d,(center[0],614),"Phase advance",17,MUTED,anchor="ma") text(d,(center[0],708),"Less than one turn",19,MUTED,anchor="ma") else: text(d,(center[0],614),"Completed turns",17,MUTED,anchor="ma") text(d,(center[0],697),"Remainder angle",17,MUTED,anchor="ma") text(d,(39,818),"Each trial continues from the previous stop",20,MUTED) text(d,(1400,821),"θ = φ mod 2π · θ determines the final direction",20,GOLD,anchor="ra") return image def frame(index): stage,progress,stopped=state(index) image=background().copy() d=ImageDraw.Draw(image,"RGBA") length=PATH_LENGTHS[stage] text(d,(1400,72),f"Trial {stage+1} / 3 L = {length:g}",20,INK,anchor="ra") for pane,center in enumerate(CENTERS): phase=float(phase_at(stage,progress)[pane]) whole=int(math.floor(phase/(2*np.pi)+1e-12)) theta=phase%(2*np.pi) # Historical endpoints remain where those earlier runs actually ended. for previous in range(stage): old_angle=float(REMAINDERS[previous,pane]) old_tip=endpoint(center,old_angle) arrow(d,center,old_tip,(*BLUE,72),width=1.8,head=8) circle(d,old_tip,4,(*BLUE,165),1.4) if pane==1: text(d,endpoint(center,old_angle,RADIUS+25),str(previous+1),17,MUTED,anchor="mm") # A short trail exposes rotation direction without drawing extra turns. trail_angle=min(.44,phase) if progress>0 and not stopped: angles=np.linspace(phase-trail_angle,phase,24) for a,b in zip(angles[:-1],angles[1:]): line(d,endpoint(center,a,RADIUS-5),endpoint(center,b,RADIUS-5),(*BLUE,75),2.) # The final remainder sector appears after the complete phase advance. if stopped and pane==1: arc(d,center,theta,45,GOLD,2.2) dot(d,endpoint(center,theta,45),GOLD,2.8) tip=endpoint(center,phase) arrow(d,center,tip,BLUE,width=3.7,head=12) dot(d,center,INK,3.5) dot(d,tip,GOLD if stopped else BLUE,4.5) if stopped and pane==1: text(d,endpoint(center,theta,RADIUS+25),str(stage+1),17,BLUE,bold=True,anchor="mm") if pane==0: text(d,(center[0],640),f"{math.degrees(phase):.1f}°",36,BLUE,bold=True,anchor="ma") else: text(d,(center[0],640),str(whole),36,INK,bold=True,anchor="ma") remainder_label=f"{math.degrees(theta):.1f}°" if stopped else "…" text(d,(center[0],724),remainder_label,29,GOLD,bold=stopped,anchor="ma") return image.reduce(SUPERSAMPLE) def checks(): assert np.allclose(np.diff(PATH_LENGTHS),1.,rtol=0,atol=1e-14) assert np.allclose(PHASES[:,1],112.8*PHASES[:,0],rtol=0,atol=6e-14) assert np.all((TURNS[:,0]>0)&(TURNS[:,0]<=.25)) assert np.allclose(np.exp(1j*PHASES),np.exp(1j*REMAINDERS),atol=2e-14) assert np.allclose(np.degrees(REMAINDERS),[[30,144],[60,288],[90,72]],atol=2e-11) all_phases=np.array([phase_at(*state(i)[:2]) for i in range(TOTAL_FRAMES)]) assert np.all(np.diff(all_phases,axis=0)>=-1e-12) assert np.allclose(all_phases[-1,0],np.pi/2) max_step=0. for stage in range(3): start=PRELUDE_FRAMES+stage*PASS_FRAMES assert state(start)==(stage,0.,False) phases=np.array([phase_at(stage,state(i)[1]) for i in range(start,start+round(SPIN*FPS)+1)]) if stage: assert np.allclose(phases[0],PHASES[stage-1]) max_step=max(max_step,float(np.max(np.diff(phases,axis=0)))) assert state(final_frame(stage))[0]==stage and state(final_frame(stage))[2] assert max_step10 tip_errors.append(matches) checked.append({"frame":index,"mean_absolute_rgb_error":error,"matching_tip_pixels":tip_errors}) if index==final_frame(stage): selected[stage]=Image.fromarray(decoded.copy()) assert count==TOTAL_FRAMES and len(selected)==3 sheet=Image.new("RGB",(1440,1305),BG) for stage in range(3): sheet.paste(selected[stage].resize((720,435),Image.Resampling.LANCZOS),(360,stage*435)) sheet.save(OUT/f"{NAME}-encoded-contact-sheet.png") report={"decoded_frames":count,"duration_seconds":count/FPS,"checks":checked} (OUT/f"{NAME}-encoded-validation.json").write_text(json.dumps(report,indent=2)+"\n",encoding="utf-8") print(json.dumps({"decoded_frames":count,"checked":len(checked), "max_rgb_error":max(x["mean_absolute_rgb_error"] for x in checked)}),flush=True) if __name__=="__main__": parser=argparse.ArgumentParser() for option in ("check","preview","render","encoded-check"): parser.add_argument("--"+option,action="store_true") args=parser.parse_args() if args.check: checks() if args.preview: previews() if args.render: render() if args.encoded_check: encoded_checks()