from pathlib import Path
import math,csv,json,sys,hashlib,itertools
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
P=Path(__file__).resolve().parent

def reading(a,v,A,V,b=0):
 if not all(math.isfinite(x)for x in (a,v,b))or not 0<=a<=1 or not 0<=v<=1 or A not in (0,1)or V not in (0,1):raise ValueError('Invalid assigned model input')
 return b+a*A+v*V
rows=[dict(acoustic_coefficient=i/20,vibration_coefficient=j/20,baseline=0,acoustic_cue=A,vibration_cue=V,reading=reading(i/20,j/20,A,V))for i,j,A,V in itertools.product(range(21),range(21),(0,1),(0,1))]
maxerr=0
for i,j in itertools.product(range(21),repeat=2):
 a,v=i/20,j/20;y00,y01,y10,y11=[reading(a,v,A,V)for A,V in itertools.product((0,1),repeat=2)]
 maxerr=max(maxerr,abs((y10-y00)-a),abs((y01-y00)-v),abs(y11-y10-y01+y00))
 assert abs(y11-(a+v))<1e-14
assert maxerr<1e-14
full=np.array([[1,A,V]for A,V in itertools.product((0,1),repeat=2)])
diagonal=np.array([[1,0,0],[1,1,1]])
assert np.linalg.matrix_rank(full)==3 and np.linalg.matrix_rank(diagonal)==2
assert np.array_equal(diagonal@np.array([0,1,-1]),np.zeros(2))
examples=[(.8,.2),(.2,.8),(.5,.5)]
assert all(reading(a,v,0,0)==0 and reading(a,v,1,1)==1 for a,v in examples)
guards=0
for args in [(-.1,.2,0,1),(1.1,.2,0,1),(.2,-.1,0,1),(.2,math.inf,0,1),(.2,.3,2,1),(.2,.3,0,-1),(.2,.3,0,1,math.nan)]:
 try:reading(*args)
 except ValueError:guards+=1
 else:raise AssertionError(args)
with(P/'cue-grid.csv').open('w',newline='',encoding='utf8')as f:w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
plt.rcParams.update({'font.size':11,'axes.spines.top':False,'axes.spines.right':False,'savefig.dpi':180})
fig,ax=plt.subplots(figsize=(6.5,5.3),layout='constrained');values=np.array([[reading(.8,.2,A,V)for A in (0,1)]for V in (0,1)])
im=ax.imshow(values,cmap='Blues',vmin=0,vmax=1);fig.colorbar(im,ax=ax,label='Assigned instrument reading (arbitrary units)')
for V,A in itertools.product((0,1),repeat=2):ax.text(A,V,f'{values[V,A]:.1f}',ha='center',va='center',fontsize=22,color='white'if values[V,A]>.5 else'black')
ax.set(xticks=[0,1],yticks=[0,1],xticklabels=['Absent (0)','Present (1)'],yticklabels=['Absent (0)','Present (1)'],xlabel='Acoustic cue A',ylabel='Support vibration V',title='Four separate cue conditions\nAssigned a = 0.8, v = 0.2, baseline = 0')
for e in ('png','svg','pdf'):fig.savefig(P/f'four-conditions.{e}')
plt.close(fig)
fig,ax=plt.subplots(figsize=(9,5),layout='constrained');conditions=[(0,0),(1,1),(1,0),(0,1)];x=np.arange(4)
for k,(a,v)in enumerate(examples):ax.bar(x+(k-1)*.23,[reading(a,v,A,V)for A,V in conditions],width=.22,label=f'a = {a:.1f}, v = {v:.1f}',hatch=['','//','..'][k])
ax.axvline(1.5,color='gray',ls='--');ax.set(xticks=x,xticklabels=['Neither (00)','Both (11)','Acoustic only (10)','Vibration only (01)'],ylabel='Assigned reading (arbitrary units)',title='The same paired readings can hide different contributions\nSeparated cues distinguish these additive examples',ylim=(0,1.3));ax.legend(ncol=3,fontsize=9);ax.grid(axis='y',alpha=.2)
for e in ('png','svg','pdf'):fig.savefig(P/f'confounded-cues.{e}')
plt.close(fig)
(P/'validation.json').write_text(json.dumps(dict(all_pass=True,rows=len(rows),coefficient_pairs=441,max_contrast_additivity_error=maxerr,full_design_rank=3,paired_design_rank=2,indistinguishable_examples=3,guards=guards),indent=2)+'\n')
(P/'study.json').write_text(json.dumps(dict(runtime=dict(python=sys.version,numpy=np.__version__,matplotlib=matplotlib.__version__),generator_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),formula='y=b+a*A+v*V',baseline=0,coefficient_grid=[0,1,.05],binary_cues=True,reading_units='arbitrary instrument units',interaction_assumed_zero=True,empirical_data=False,behavioral_probability=False),indent=2)+'\n')
print('PASS1764rows')
