#!/usr/bin/env python3
# SPDX-License-Identifier: CC-BY-4.0
# Creator: deep-learning-editor; source: AI Encyclopedia CN, Chapter 12.
"""Deterministically generate/check Chapter 12 tiny-network teaching data."""
import argparse, json, math
from pathlib import Path
ROUND=9; EPS=1e-6; ATOL=1e-7

def matvec(W,x,b): return [sum(w*v for w,v in zip(row,x))+bb for row,bb in zip(W,b)]
def sigmoid(a): return 1/(1+math.exp(-a))
def outer(u,v): return [[a*b for b in v] for a in u]
def rnd(x): return round(x,ROUND)
def rv(v): return [rnd(x) for x in v]
def rm(m): return [rv(r) for r in m]
def loss_for(x,W1,b1,W2,b2,y):
 z=matvec(W1,x,b1); h=[max(0,v) for v in z]; a=sum(w*v for w,v in zip(W2[0],h))+b2
 # stable BCE from logit
 return max(a,0)-y*a+math.log1p(math.exp(-abs(a)))

def finite_difference(x,W1,b1,W2,b2,y,analytic):
 groups={'W1':W1,'b1':b1,'W2':W2,'b2':[b2]}; maximum=0.0; count=0
 for name,obj in groups.items():
  rows=obj if obj and isinstance(obj[0],list) else [obj]
  for i,row in enumerate(rows):
   for j in range(len(row)):
    old=row[j]; row[j]=old+EPS; lp=loss_for(x,W1,b1,W2,groups['b2'][0],y); row[j]=old-EPS; lm=loss_for(x,W1,b1,W2,groups['b2'][0],y); row[j]=old
    numerical=(lp-lm)/(2*EPS)
    expected=analytic[name][i][j] if name in ('W1','W2') else (analytic['b1'][j] if name=='b1' else analytic['b2'])
    maximum=max(maximum,abs(numerical-expected)); count+=1
 if maximum>ATOL: raise ArithmeticError(f'finite-difference mismatch max_abs_error={maximum}')
 return count,maximum

def payload():
 x=[1.,-1.];W1=[[.5,-.5],[-.25,-.75]];b1=[0.,0.];W2=[[.8,-.4]];b2=.1;y=1;eta=.1
 z1=matvec(W1,x,b1);h=[max(0,z) for z in z1];a=sum(w*v for w,v in zip(W2[0],h))+b2;p=sigmoid(a);L=loss_for(x,W1,b1,W2,b2,y)
 da=p-y;dW2=outer([da],h);dh=[W2[0][i]*da for i in range(2)];dz1=[g*(1 if z>0 else 0) for g,z in zip(dh,z1)];dW1=outer(dz1,x);analytic={'W1':dW1,'b1':dz1,'W2':dW2,'b2':da};count,err=finite_difference(x,W1,b1,W2,b2,y,analytic)
 W1n=[[W1[i][j]-eta*dW1[i][j] for j in range(2)] for i in range(2)];b1n=[b1[i]-eta*dz1[i] for i in range(2)];W2n=[[W2[0][j]-eta*dW2[0][j] for j in range(2)]];b2n=b2-eta*da
 z1n=matvec(W1n,x,b1n);hn=[max(0,z) for z in z1n];an=sum(w*v for w,v in zip(W2n[0],hn))+b2n;pn=sigmoid(an);Ln=loss_for(x,W1n,b1n,W2n,b2n,y)
 return {'schema':1,'creator':'deep-learning-editor','license':'CC BY 4.0','license_url':'https://creativecommons.org/licenses/by/4.0/','source':'AI Encyclopedia CN, Chapter 12','provenance':'Original fictional teaching network; no people or deployment data.','conventions':{'vectors':'column vectors in prose; JSON stores flat arrays','loss':'stable natural-log binary cross-entropy, y=1','relu_at_zero':'derivative convention 0; example and perturbations avoid zero','rounding':'derived JSON numbers rounded to 9 decimal places; calculations use binary64'},'shapes':{'x':[2,1],'W1':[2,2],'b1':[2,1],'z1':[2,1],'h':[2,1],'W2':[1,2],'b2':[],'a':[],'p':[]},'input':{'x':x,'W1':W1,'b1':b1,'W2':W2,'b2':b2,'y':y,'learning_rate':eta},'forward':{'z1':rv(z1),'h':rv(h),'a':rnd(a),'p':rnd(p),'loss':rnd(L)},'local_derivatives':{'dp_da':rnd(p*(1-p)),'dL_dp':rnd(-1/p),'dL_da':rnd(da),'relu_prime':[1,1]},'gradients':{'W2':rm(dW2),'b2':rnd(da),'h':rv(dh),'z1':rv(dz1),'W1':rm(dW1),'b1':rv(dz1)},'finite_difference':{'epsilon':EPS,'absolute_tolerance':ATOL,'scalar_parameters_checked':count,'max_absolute_error':rnd(err),'passed':True},'updated':{'W1':rm(W1n),'b1':rv(b1n),'W2':rm(W2n),'b2':rnd(b2n),'z1':rv(z1n),'a':rnd(an),'p':rnd(pn),'loss':rnd(Ln),'loss_decreased':Ln<L}}

def encode(): return (json.dumps(payload(),ensure_ascii=False,indent=2,sort_keys=True)+'\n').encode()
def default_output():
 here=Path(__file__).resolve()
 return here.parent if here.parent.name=='data' and here.parent.parent.name=='public' else here.parents[1]/'public/data'
def main():
 ap=argparse.ArgumentParser();g=ap.add_mutually_exclusive_group(required=True);g.add_argument('--check',action='store_true');g.add_argument('--write',action='store_true');ap.add_argument('--output-dir',type=Path);a=ap.parse_args();out=(a.output_dir or default_output())/'ch12-tiny-network.json';expected=encode()
 if a.check:
  if not out.is_file() or out.read_bytes()!=expected: raise SystemExit(f'FAIL: {out} missing or differs')
  print(f'PASS: {out} deterministic; 9/9 finite-difference gradients within {ATOL:g}')
 else:
  out.parent.mkdir(parents=True,exist_ok=True);out.write_bytes(expected);print(f'WROTE: {out}')
if __name__=='__main__': main()
