"""Ideal camera scales, exposure accounting and exact cosine sampling alias."""
import math,csv,json
from pathlib import Path
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
P=Path(__file__).resolve().parent

def camera(diameter_m,range_m,focal_m):
 if not all(math.isfinite(v) and v>0 for v in [diameter_m,range_m,focal_m]):raise ValueError('Positive finite geometry required')
 angle=1.22*550e-9/diameter_m;pitch=range_m*5e-6/focal_m;fov=2*math.atan(1920*5e-6/(2*focal_m))
 return dict(aperture_m=diameter_m,range_m=range_m,focal_length_m=focal_m,rayleigh_angle_rad=angle,rayleigh_linear_m=range_m*angle,projected_pixel_pitch_m=pitch,horizontal_FOV_rad=fov)

def exposure(t):
 if not math.isfinite(t) or t<0:raise ValueError('Finite nonnegative exposure required')
 signal=1000*t;bg=200*t;variance=signal+bg+3**2
 return dict(exposure_s=t,signal_electrons=signal,background_electrons=bg,read_noise_electrons_rms=3,snr=signal/math.sqrt(variance),transverse_blur_m=.1*t)
cam=[camera(d/1000,r,f/1000) for d in [10,25,50] for r in [1,10,100] for f in [25,50,100]]
exp=[exposure(i/1000) for i in range(1001)]
alias=[dict(sample_index=k,time_s=k/20,low_frequency_Hz=3,high_frequency_Hz=17,low_sample=math.cos(2*math.pi*3*k/20),high_sample=math.cos(2*math.pi*17*k/20)) for k in range(21)]
for name,rows in [('camera.csv',cam),('exposure.csv',exp),('alias.csv',alias)]:
 with (P/name).open('w',newline='') as f:w=csv.DictWriter(f,fieldnames=rows[0]);w.writeheader();w.writerows(rows)
maxalias=max(abs(r['low_sample']-r['high_sample']) for r in alias);assert maxalias<1e-12
fov_err=max(abs(2*r['range_m']*math.tan(r['horizontal_FOV_rad']/2)-1920*r['projected_pixel_pitch_m']) for r in cam);assert fov_err<1e-12
assert all(math.isclose(r['rayleigh_angle_rad']*r['aperture_m'],1.22*550e-9,rel_tol=1e-14) for r in cam)
assert exp[0]['snr']==0 and exp[0]['transverse_blur_m']==0
assert all(a['snr']<b['snr'] for a,b in zip(exp,exp[1:]))
assert all(math.isclose(r['snr']**2*(r['signal_electrons']+r['background_electrons']+9),r['signal_electrons']**2,rel_tol=1e-12,abs_tol=1e-12) for r in exp)
plt.rcParams.update({'font.family':'DejaVu Sans','font.size':10,'axes.spines.top':False,'axes.spines.right':False,'svg.fonttype':'none','pdf.fonttype':42})
fig,ax=plt.subplots(1,2,figsize=(10.5,4.6))
d=np.linspace(10,50,201);ax[0].plot(d,1.22*550e-9/(d/1000)*10*1000,label='Rayleigh separation',color='#7550a0');ax[0].axhline(10*5e-6/.05*1000,label='Projected pixel pitch',color='#087e91',ls='--');ax[0].set(xlabel='Ideal circular aperture diameter (mm)',ylabel='Object-plane length scale (mm)',title='A  |  Different limits describe different things');ax[0].legend(fontsize=8)
x=[r['exposure_s'] for r in exp];ax[1].plot(x,[r['snr'] for r in exp],color='#087e91',label='SNR');ax[1].set(xlabel='Exposure time (s)',ylabel='Signal-to-noise ratio',title='B  |  Longer exposure gathers more signal');tw=ax[1].twinx();tw.plot(x,[r['transverse_blur_m']*1000 for r in exp],color='#a74687',ls='--');tw.set_ylabel('Object-plane motion blur (mm)',color='#a74687')
for a in ax:a.grid(alpha=.2)
fig.suptitle('Imaging needs optics, sampling and exposure',fontsize=15);fig.text(.5,.05,'A: wavelength 550 nm, range 10 m, focal length 50 mm, pixel pitch 5 micrometres. These are not interchangeable resolution criteria.',ha='center',fontsize=8);fig.text(.5,.014,'B: assigned 1000 signal + 200 background electrons/s, 3-electron RMS read noise, transverse speed 0.1 m/s; idealized fixed geometry.',ha='center',fontsize=8);fig.tight_layout(rect=[0,.10,1,.92])
for ext in ['png','svg','pdf']:fig.savefig(P/f'imaging-budgets.{ext}',dpi=190)
fig,ax=plt.subplots(figsize=(9,4.3));t=np.linspace(0,1,2001);ax.plot(t,np.cos(2*np.pi*3*t),color='#087e91',label='3 Hz continuous cosine');ax.plot(t,np.cos(2*np.pi*17*t),color='#a74687',alpha=.65,label='17 Hz continuous cosine');ax.scatter([r['time_s'] for r in alias],[r['low_sample'] for r in alias],color='#222',zorder=5,label='Identical samples at 20 samples/s');ax.set(xlabel='Time (s)',ylabel='Normalized signal',title='Two different waves, exactly the same discrete samples');ax.legend(fontsize=8,loc='upper right');ax.grid(alpha=.2);fig.text(.5,.02,'Analytic sinusoids, not photographs. Without a band-limit or other prior information, these samples cannot distinguish the two signals.',ha='center',fontsize=8);fig.tight_layout(rect=[0,.055,1,1])
for ext in ['png','svg','pdf']:fig.savefig(P/f'sampling-alias.{ext}',dpi=190)
meta={'title':'Ideal imaging scales and sampling ambiguity','status':'Analytical illustration with hypothetical detector and motion inputs; no measured image or device','rows':{'camera':27,'exposure':1001,'alias':21},'parameters':{'wavelength_m':550e-9,'pixel_pitch_m':5e-6,'horizontal_pixels':1920,'aperture_m':[.01,.025,.05],'range_m':[1,10,100],'focal_length_m':[.025,.05,.1],'signal_electrons_per_s':1000,'background_electrons_per_s':200,'read_noise_electrons_rms':3,'transverse_speed_m_s':.1,'sample_rate_Hz':20,'alias_frequencies_Hz':[3,17]},'formulas':{'Rayleigh':'theta=1.22*lambda/D; linear scale=range*theta (small-angle)','projected_pitch':'range*pixel_pitch/focal_length','FOV':'2*atan(N*pixel_pitch/(2*focal_length))','SNR':'signal/sqrt(signal+background+read_noise²)','blur':'transverse_speed*exposure','alias':'cos(2*pi*f*k/fs)=cos(2*pi*(fs-f)*k/fs), integer k'},'checks':{'all_pass':True,'alias_max_absolute_difference':maxalias,'FOV_width_identity_max_error_m':fov_err,'Rayleigh_scaling':True,'SNR_variance_identity':True,'zero_time':True,'SNR_monotone':True},'limitations':['Rayleigh criterion is for ideal unobstructed circular diffraction-limited aperture and incoherent point sources; not a universal feature-resolution threshold.','Sampling pitch is not resolved detail. No pixel response, aberration, defocus, contrast transfer, atmosphere or reconstruction algorithm is modeled.','Projection and field of view use ideal rectilinear pinhole geometry at fixed range; no finite-conjugate lens correction.','SNR assumes independent Poisson signal/background counts and independent read noise for one measurement. Background mean is known without additional estimation noise; dark current, saturation, quantization and fixed-pattern noise are excluded.','Motion blur is transverse displacement at constant range and speed, not a full image convolution or recoverability metric.','Alias example contains two deterministic cosines, not a fabricated image. No clinical, identification or surveillance performance claim.']}
(P/'study.json').write_text(json.dumps(meta,indent=2)+'\n');(P/'validation.json').write_text(json.dumps(meta['checks'],indent=2)+'\n');print(meta['checks'])
