#!/usr/bin/env python3
"""Draw corpus statistics and within-paper comparisons; never overwrite files.

Requires matplotlib and numpy. All plotted observations are listed in plot_data.json.
"""
from pathlib import Path
import json
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
from matplotlib.patches import Patch
import numpy as np

HERE=Path(__file__).resolve().parent
FIGURES=HERE/'figures'
INK='#193047'; MUTED='#627486'; TEAL='#187f79'; BLUE='#6685bd'
ORANGE='#c47b36'; GREY='#aab4bf'; PALE='#f0f3f5'; PAPER='#ffffff'
plt.rcParams.update({
    'font.family':'DejaVu Sans','font.size':10,'text.color':INK,
    'axes.labelcolor':INK,'xtick.color':MUTED,'ytick.color':MUTED,
    'axes.titleweight':'bold','axes.titlesize':12,'axes.labelsize':10,
    'axes.edgecolor':'#cbd2d9','axes.spines.top':False,'axes.spines.right':False,
    'figure.facecolor':PAPER,'axes.facecolor':PAPER,'savefig.facecolor':PAPER,
    'grid.color':'#e7ebee','grid.linewidth':0.7,'axes.axisbelow':True,
    'svg.fonttype':'none','savefig.dpi':185,
})

TRIADIC=[
 # name, state MB, parameters M, recall %, PG19 16K-64K PPL, group, PDF page
 ('GDN base',6.3,380.9,26.2,14.15,'triadic',6),
 ('Larger heads (2x)',12.6,380.7,26.6,14.17,'alternative',6),
 ('Grouped values (2x)',12.6,378.2,27.1,14.12,'alternative',6),
 ('Wider values (2x)',12.6,377.9,27.4,14.14,'alternative',6),
 ('More heads (2x)',12.6,381.6,27.5,14.28,'alternative',6),
 ('Triadic E=2',12.6,381.9,28.4,13.95,'triadic',6),
 ('Larger heads (4x)',25.2,380.6,28.5,14.24,'alternative',6),
 ('Grouped values (4x)',25.2,382.4,27.3,14.36,'alternative',6),
 ('Wider values (4x)',25.2,381.3,27.5,14.44,'alternative',6),
 ('Triadic E=4',25.2,383.0,31.1,13.79,'triadic',6),
 ('Triadic E=8, scratch',50.3,385.4,33.1,13.74,'triadic',7),
 ('Triadic E=8, upcycled',50.3,385.4,29.7,13.86,'upcycled',7),
]
LATTICE=[[16,15.52],[32,15.26],[64,15.02],[96,14.89],[128,14.84],[160,14.76],[192,14.71]]

def save(fig,name):
    for suffix in ('.png','.svg'):
        p=FIGURES/(name+suffix)
        if p.exists():
            raise FileExistsError(f'Refusing to overwrite {p}')
    for suffix in ('.png','.svg'):
        fig.savefig(FIGURES/(name+suffix),bbox_inches='tight',pad_inches=0.22)
    plt.close(fig)

def landscape(corpus):
    fig=plt.figure(figsize=(13.2,7.5))
    gs=fig.add_gridspec(1,2,width_ratios=[2.3,1],wspace=.19)
    ax=fig.add_subplot(gs[0,0]); ax2=fig.add_subplot(gs[0,1])
    lookup={p['name']:p for p in corpus['papers']}
    labels=[]; direct=[];nearby=[]
    for group in corpus['groups']:
        labels.append(group['en'])
        direct.append(sum(lookup[n]['tier']=='A' for n in group['members']))
        nearby.append(sum(lookup[n]['tier']=='B' for n in group['members']))
    y=np.arange(len(labels))
    ax.barh(y,direct,color=TEAL,height=.64,label='A: directly related (45)')
    ax.barh(y,nearby,left=direct,color=BLUE,height=.64,label='B: adjacent mechanisms (23)')
    for yy,a,b in zip(y,direct,nearby):
        ax.text(a+b+.22,yy,str(a+b),va='center',fontweight='bold',fontsize=11)
    ax.set_yticks(y,labels);ax.invert_yaxis();ax.set_xlim(0,13)
    ax.set_xticks(range(0,14,2));ax.set_xlabel('Unique papers in this library')
    ax.grid(axis='x');ax.tick_params(axis='y',length=0)
    ax.set_title('68 related papers across nine primary categories',loc='left',pad=18)
    ax.legend(loc='lower left',bbox_to_anchor=(0,-.24),ncol=1,frameon=False,fontsize=10)
    ax2.pie([45,23,67],colors=[TEAL,BLUE,'#dfe4e9'],startangle=90,counterclock=False,
            wedgeprops={'width':.25,'edgecolor':'white','linewidth':3})
    ax2.text(0,.08,'135',ha='center',fontsize=34,fontweight='bold')
    ax2.text(0,-.17,'unique papers',ha='center',color=MUTED,fontsize=11)
    ax2.set_title('Local library snapshot',pad=15)
    ax2.text(0,-1.35,'45 direct  +  23 adjacent\n67 outside the mechanism count',
             ha='center',va='top',fontsize=11,linespacing=1.7)
    fig.suptitle('Linear attention: a dense landscape of memory designs',x=.02,y=.99,
                 ha='left',fontsize=17,fontweight='bold')
    fig.text(.02,.005,'2026-10-20 | Local corpus, not a global publication census. '
             'Primary categories are exclusive; paper versions and note revisions are deduplicated.',fontsize=9,color=MUTED)
    fig.subplots_adjust(left=.265,right=.98,top=.87,bottom=.2)
    save(fig,'01-corpus-landscape')

def mechanism_map(data):
    rows=data['rows']; cols=data['columns']; matrix=np.zeros((len(rows),len(cols)))
    for i,row in enumerate(rows):
        for j in row['features']:
            matrix[i,j]=1 if row['tier']=='A' else 2
    fig,ax=plt.subplots(figsize=(12.2,13.8))
    ax.imshow(matrix,cmap=ListedColormap([PALE,TEAL,BLUE]),vmin=0,vmax=2,
              aspect='auto',interpolation='nearest')
    xlabels=['Content\ncapacity +','Auxiliary\nstate +','Routing /\nslots','Write /\nerase',
             'Time /\nphase','Readout','Training /\ninput','Storage /\nI/O','Explicit\nKV']
    ax.set_xticks(np.arange(len(cols)),xlabels,fontsize=10)
    ax.xaxis.tick_top();ax.tick_params(axis='x',pad=10,length=0)
    ax.set_yticks(np.arange(len(rows)),[r['name'] for r in rows],fontsize=10)
    ax.tick_params(axis='y',pad=10,length=0)
    ax.set_xticks(np.arange(-.5,len(cols),1),minor=True)
    ax.set_yticks(np.arange(-.5,len(rows),1),minor=True)
    ax.grid(which='minor',color='white',linewidth=3)
    ax.tick_params(which='minor',bottom=False,left=False)
    for spine in ax.spines.values():spine.set_visible(False)
    for i in range(len(rows)):
        for j in range(len(cols)):
            if matrix[i,j]:ax.text(j,i,'•',ha='center',va='center',fontsize=12,color='white')
    fig.suptitle('Many papers act on neighboring parts of the same memory lifecycle',
                 x=.02,y=.995,ha='left',fontsize=16,fontweight='bold')
    fig.text(.02,.961,'34 representative works | Qualitative intervention map; '
             'colored cells are not performance scores.',fontsize=11,color=MUTED)
    fig.legend(handles=[Patch(facecolor=TEAL,label='A: direct'),Patch(facecolor=BLUE,label='B: adjacent'),
                        Patch(facecolor=PALE,label='Not the highlighted intervention')],
               loc='lower left',bbox_to_anchor=(.17,.035),ncol=3,frameon=False,fontsize=10)
    fig.text(.02,.018,'Extra content capacity and auxiliary memory are separate. '
             'Flags describe the design relative to its base path; they do not assert equal-resource gains.',fontsize=9,color=MUTED)
    fig.subplots_adjust(left=.21,right=.99,top=.87,bottom=.10)
    save(fig,'02-mechanism-overlap')

def triadic():
    fig,axes=plt.subplots(1,2,figsize=(12.6,5.9))
    selected=[r for r in TRIADIC if r[5]=='triadic'];alts=[r for r in TRIADIC if r[5]=='alternative']
    up=[r for r in TRIADIC if r[5]=='upcycled'][0]
    for ax,idx,title,ylabel,ref in zip(axes,[3,4],['Recall improves, with a remaining gap','Long-context prediction improves'],
                                     ['Six-task recall average (%)','PG19 16K–64K perplexity'],[41.6,13.92]):
        ax.scatter([r[1] for r in alts],[r[idx] for r in alts],s=49,color=GREY,
                   edgecolor='white',linewidth=.6,zorder=3,label='Other enlargements (Table 1)')
        ax.plot([r[1] for r in selected],[r[idx] for r in selected],'-o',color=TEAL,
                linewidth=2.6,markersize=7,label='Triadic, trained from scratch',zorder=4)
        ax.scatter(up[1],up[idx],marker='D',s=78,color=ORANGE,edgecolor='white',
                   zorder=5,label='E=8, upcycled')
        ax.axhline(ref,color=BLUE,linestyle=(0,(4,3)),linewidth=1.5,
                   label='Transformer (growing KV; reference)')
        for r in selected:
            label=f'{r[idx]:.1f}' if idx==3 else f'{r[idx]:.2f}'
            offset=(0,11) if idx==3 else (0,-18)
            ax.annotate(label,(r[1],r[idx]),xytext=offset,textcoords='offset points',
                        ha='center',color=TEAL,fontsize=10,fontweight='bold')
        ax.annotate(f'{up[idx]:.1f}' if idx==3 else f'{up[idx]:.2f}',(up[1],up[idx]),
                    xytext=(-7,12) if idx==3 else (-8,-6),textcoords='offset points',
                    ha='right',color=ORANGE,fontsize=10)
        ax.set_xscale('log',base=2);ax.set_xticks([6.3,12.6,25.2,50.3],['6.3','12.6','25.2','50.3'])
        ax.set_xlim(5.2,61);ax.set_xlabel('Content state (MB; 2 bytes / entry)')
        ax.set_ylabel(ylabel);ax.set_title(title,loc='left',pad=15);ax.grid(alpha=.8)
    axes[0].set_ylim(23,44);axes[0].set_yticks([25,30,35,40])
    axes[1].set_ylim(13.57,14.57)
    handles,labels=axes[0].get_legend_handles_labels()
    fig.legend(handles,labels,loc='lower center',bbox_to_anchor=(.5,.065),ncol=2,frameon=False,fontsize=9.5)
    fig.suptitle('Capacity is useful; its organization and training also matter',
                 x=.02,y=.99,ha='left',fontsize=17,fontweight='bold')
    fig.text(.02,.018,'Redrawn from Triadic v1, Tables 1–2 (PDF pp.6–7). '
             '400M-class GDN; means of 3 seeds. Parameters vary slightly; no invented error bars.',fontsize=9,color=MUTED)
    fig.subplots_adjust(left=.07,right=.98,wspace=.26,top=.81,bottom=.29)
    save(fig,'03-triadic-capacity')

def utilization():
    fig,axes=plt.subplots(1,2,figsize=(12.6,5.9),gridspec_kw={'wspace':.29})
    ax=axes[0];m,ppl=np.array(LATTICE).T
    ax.plot(m,ppl,'-o',color=TEAL,linewidth=2.6,markersize=6,label='Lattice')
    ax.scatter([64],[15.60],marker='D',s=80,color=ORANGE,label='TTT, 64 slots',zorder=5)
    ax.scatter([64],[16.35],marker='s',s=60,color=BLUE,label='DeltaNet, 64 slots',zorder=5)
    ax.annotate('16 slots: 15.52',(16,15.52),xytext=(9,14),textcoords='offset points',color=TEAL,fontweight='bold')
    ax.annotate('15.60',(64,15.60),xytext=(8,7),textcoords='offset points',color=ORANGE)
    ax.set_xscale('log',base=2);ax.set_xticks([16,32,64,128,192],['16','32','64','128','192'])
    ax.set_xlim(13,230);ax.set_ylim(14.45,16.65)
    ax.set_ylabel('Validation perplexity (lower is better)');ax.set_xlabel('Compression memory slots m')
    ax.set_title('Lattice: memory efficiency within one paper',loc='left',pad=14)
    ax.grid(alpha=.8);ax.legend(frameon=False,loc='upper right',fontsize=9)
    ax.text(0,-.23,'110M-class models, FineWeb-Edu\nTable 3 (p.10), Figure 6 (p.23)',
            transform=ax.transAxes,fontsize=9,color=MUTED,linespacing=1.6)
    ax=axes[1]
    bars=ax.bar([0,1],[8.8,97.4],width=.55,color=[GREY,TEAL])
    for bar,value in zip(bars,[8.8,97.4]):
        ax.text(bar.get_x()+bar.get_width()/2,value+2,f'{value:.1f}%',ha='center',
                va='bottom',fontweight='bold',fontsize=13)
    ax.set_xticks([0,1],['Window 512\nGlobal 0','Window 256\nGlobal 256 (SRE)'])
    ax.set_ylim(0,111);ax.set_yticks([0,25,50,75,100]);ax.grid(axis='y')
    ax.set_ylabel('4K S-NIAH-1 accuracy (%)');ax.set_title('LoLA: same nominal explicit-cache budget',loc='left',pad=14)
    ax.text(.04,.77,'Same weights\n512 nominal KV slots',transform=ax.transAxes,ha='left',
            color=MUTED,fontsize=11,linespacing=1.6)
    ax.text(0,-.23,'LoLCATs-8B+ vs LoLA-8B\nLoLA v3, Table 1 (PDF pp.7–8)',
            transform=ax.transAxes,fontsize=9,color=MUTED,linespacing=1.6)
    fig.suptitle('Utilization requires a resource-aware comparison',x=.02,y=.99,
                 ha='left',fontsize=17,fontweight='bold')
    fig.text(.02,.018,'Each panel is a separate within-paper comparison. '
             'The two panels do not share a benchmark or establish a cross-paper ranking.',fontsize=9,color=MUTED)
    fig.subplots_adjust(left=.07,right=.98,top=.81,bottom=.27)
    save(fig,'04-utilization-evidence')

def main():
    corpus=json.loads((HERE/'corpus_inventory.json').read_text())
    mechanism=json.loads((HERE/'mechanism_map.json').read_text())
    plot_data={
        'scope':'Measured results are compared only within the named paper and protocol.',
        'triadic':{
            'source_pdf':'paper-readings/Triadic-2609.36529/v1/source/paper.pdf',
            'source_tex':['tables/main_400m_combined.tex','tables/upcycling.tex'],
            'source_tex_root':'paper-readings/Triadic-2609.36529/v1/source/tex/files',
            'columns':['model','state_mb_2bytes_per_entry','parameters_million','recall_percent','pg19_16k_64k_ppl','group','pdf_page'],
            'rows':TRIADIC,'transformer_reference':{'recall_percent':41.6,'pg19_16k_64k_ppl':13.92,'pdf_page':6},
            'protocol':'400M-class GDN; three-seed mean; 20B-token pretraining + long-context extension as in paper; E=8 upcycled is a separate training condition.',
        },
        'lattice':{
            'source_pdf':'paper-readings/Lattice-2504.05646/v2/source/paper.pdf',
            'source_tex':'paper-readings/Lattice-2504.05646/v2/source/tex/files/FigureTable/ablation_m.tex',
            'source_location':'Table 3 PDF p.10; Figure 6 PDF p.23',
            'columns':['memory_slots','validation_ppl'],'lattice_rows':LATTICE,
            'baselines':{'TTT':[64,15.60],'DeltaNet':[64,16.35]},
            'protocol':'110M-class models on FineWeb-Edu, configurations as reported; memory slot count is not full-model bytes.',
        },
        'lola':{
            'source_pdf':'paper-readings/LoLA-2505.23666/v3/source/paper.pdf',
            'source_location':'Table 1 PDF pp.7–8',
            'protocol':'Same model weights; 4K S-NIAH-1; 512 nominal explicit KV slots; not an assertion of identical peak VRAM.',
            'rows':[{'model':'LoLCATs-8B+','window':512,'global':0,'accuracy_percent':8.8},
                    {'model':'LoLA-8B','window':256,'global':256,'accuracy_percent':97.4}],
        },
        'corpus':{'input':'corpus_inventory.json','total':135,'direct':45,'adjacent':23,'outside_count':67},
        'mechanism_map':{'input':'mechanism_map.json','representative_rows':len(mechanism['rows']),
                         'meaning':'Qualitative intervention classification by this report, not empirical scores.'},
    }
    with (HERE/'plot_data.json').open('x',encoding='utf-8') as f:
        json.dump(plot_data,f,ensure_ascii=False,indent=2);f.write('\n')
    landscape(corpus);mechanism_map(mechanism);triadic();utilization()
    print('Created 4 PNG figures, 4 SVG figures, and plot_data.json.')

if __name__=='__main__':main()
