/* THESEUS ยท KG Statistics core. No network access or dependencies. Mirrors TripletsStats.calculate_navigation_calibration and common graph stats. */ (function (scope) { 'use strict'; const quantile = (sorted, p) => { if (!sorted.length) return 0; const pos = (sorted.length - 1) * p, i = Math.floor(pos), a = pos - i; return sorted[i] + (sorted[Math.min(i + 1, sorted.length - 1)] - sorted[i]) * a; }; const median = a => quantile(a, .5); const mean = a => a.length ? a.reduce((x, y) => x + y, 0) / a.length : 0; const frequencies = a => { const f = new Map(); for (const n of a) f.set(n, (f.get(n) || 0) + 1); return [...f].sort((a, b) => a[0] - b[0]).map(([degree, count]) => ({degree, count})); }; function createAccumulator() { const ids = new Map(), names = [], parent = [], rank = [], out = [], inc = [], outRaw = [], inRaw = [], neighbors = [], outgoingNeighbors = [], outgoingRels = [], incidentRels = [], relIds = new Map(), relNames = [], relFreq = [], relEdges = [], unique = new Set(); let rows = 0, simpleEdges = 0, bad = 0, selfLoops = 0; function node(label) { let id = ids.get(label); if (id !== undefined) return id; id = names.length; ids.set(label,id); names.push(label); parent.push(id); rank.push(1); out.push(0); inc.push(0); outRaw.push(0); inRaw.push(0); neighbors.push(new Set()); outgoingNeighbors.push(new Set()); outgoingRels.push(new Set()); incidentRels.push(new Set()); return id; } function relation(label) { let id = relIds.get(label); if (id !== undefined) return id; id = relNames.length; relIds.set(label,id); relNames.push(label); relFreq.push(0); relEdges.push([]); return id; } function root(x) { let y=x; while(parent[y]!==y)y=parent[y];while(parent[x]!==x){const k=parent[x];parent[x]=y;x=k;}return y; } function unite(a,b) {a=root(a);b=root(b);if(a===b)return;if(rank[a]a-b), inValues=[...inc].sort((a,b)=>a-b), onValues=outgoingNeighbors.map(s=>s.size).sort((a,b)=>a-b), orValues=outgoingRels.map(s=>s.size).sort((a,b)=>a-b); const sums=new Map();for(let i=0;ia-b), largest=wcc[wcc.length-1]||0; const s={ triplet_rows:rows, unique_triples:unique.size, duplicate_triplet_rows:rows-unique.size, entities:n,relations:m,self_loop_triples:selfLoops,zero_out_degree_nodes:out.filter(v=>v===0).length, out_degree_mean:mean(outValues),out_degree_median:median(outValues),out_degree_p90:quantile(outValues,.9), out_degree_p95:quantile(outValues,.95),out_degree_p99:quantile(outValues,.99),out_degree_max:outValues[n-1]||0, in_degree_mean:mean(inValues),in_degree_median:median(inValues),in_degree_p90:quantile(inValues,.9), in_degree_p95:quantile(inValues,.95),in_degree_p99:quantile(inValues,.99),in_degree_max:inValues[n-1]||0, unique_out_neighbors_mean:mean(onValues),unique_out_neighbors_median:median(onValues), unique_out_neighbors_p90:quantile(onValues,.9), unique_out_neighbors_p95:quantile(onValues,.95), unique_out_neighbors_p99:quantile(onValues,.99),unique_out_neighbors_max:onValues[n-1]||0, distinct_out_relations_mean:mean(orValues),distinct_out_relations_median:median(orValues), distinct_out_relations_p90:quantile(orValues,.9),distinct_out_relations_p95:quantile(orValues,.95), distinct_out_relations_p99:quantile(orValues,.99),distinct_out_relations_max:orValues[n-1]||0, nodes_out_degree_gt_10_fraction: n?out.filter(v=>v>10).length/n:0, nodes_out_degree_gt_50_fraction:n?out.filter(v=>v>50).length/n:0, nodes_out_degree_gt_100_fraction:n?out.filter(v=>v>100).length/n:0, wcc_count:wcc.length,largest_wcc_nodes:largest,largest_wcc_fraction:n?largest/n:0, median_wcc_nodes:median(wcc),singleton_wcc_count:wcc.filter(v=>v===1).length, isolated_nodes:out.reduce((sum,v,i)=>sum+(v===0 && inc[i]===0?1:0),0), out_degree_mean_legacy:mean(outRaw.filter((v,i)=>v>0||inRaw[i]>0)), in_degree_mean_legacy:mean(inRaw.filter((v,i)=>outRaw[i]>0||v>0)), graph_density:n>1?2*simpleEdges/(n*(n-1)):0 }; const nodes=names.map((name,i)=>({name,out_degree:out[i],in_degree:inc[i],out_degree_rows:outRaw[i],in_degree_rows:inRaw[i],unique_out_neighbors:outgoingNeighbors[i].size,distinct_out_relations:outgoingRels[i].size,distinct_incident_relations:incidentRels[i].size,total_degree_rows:outRaw[i]+inRaw[i]})); const maxTop=options.maxTop||25; const topNodes=[...nodes].sort((a,b)=>b.total_degree_rows-a.total_degree_rows || a.name.localeCompare(b.name)).slice(0,maxTop); const diverseNodes=[...nodes].sort((a,b)=>b.distinct_incident_relations-a.distinct_incident_relations || a.name.localeCompare(b.name)).slice(0,maxTop); const relations=relNames.map((name,i)=>({name,count:relFreq[i],unique_triples:relEdges[i].length})).sort((a,b)=>b.count-a.count||a.name.localeCompare(b.name)); const optional={}; if(options.clustering){ // Exact NetworkX-style undirected average clustering, ignoring self-loops. // Orient edges by increasing degree (then numeric node id), and count each // triangle once using intersections of forward neighbors. This avoids the // O(degree^2) cost at high-degree hubs in sparse graphs such as MetaQA. const degree=neighbors.map((adj,i)=>adj.size-(adj.has(i)?1:0)); const forward=Array.from({length:n},()=>new Set()); for(let u=0;ubudget)break; } if(estimate>budget){ optional.clustering=`Skipped. Estimated triangle-intersection work exceeds ${budget.toLocaleString('en-US')} checks. This optional calculation was skipped to avoid excessive browser resource use.`; } else { const triangles=new Float64Array(n); for(let u=0;u1)sum+=2*triangles[u]/(degree[u]*(degree[u]-1)); s.clustering_coefficient=n?sum/n:0; } } if(options.eigenvector){ // Matches NetworkX's undirected graph construction and (A + I) power iteration; no isolated metadata nodes. const connected=[];for(let i=0;i10000||simpleEdges>80000)optional.eigenvector='Skipped. Eigenvector centrality is capped at 10,000 connected nodes / 80,000 undirected edges.'; else if(connected.length){ const nn=connected.length;let x=new Float64Array(n),last=new Float64Array(n); for(const i of connected)x[i]=1/Math.sqrt(nn); let converged=false; for(let iter=0;iter<1000;iter++){ let norm=0;for(const i of connected){let v=x[i];for(const j of neighbors[i])v+=x[j];last[i]=v;norm+=v*v;} norm=Math.sqrt(norm)||1;let err=0;for(const i of connected){last[i]/=norm;err+=Math.abs(last[i]-x[i]);} [x,last]=[last,x];if(err({name:names[i],value:x[i]})).sort((a,b)=>b.value-a.value).slice(0,20); } } if(options.legacy){ if(unique.size>20000||n>10000) optional.legacy='Skipped. Legacy per-relation cycle pruning and reachability are capped at 20,000 unique triples and 10,000 nodes.'; else { const lengths=[], kh=[]; for(let r=0;r=0;i--){const src=order[i];if(scc.has(src))continue;const stack=[src];scc.set(src,sid); while(stack.length){for(const t of rev.get(stack.pop()))if(!scc.has(t)){scc.set(t,sid);stack.push(t);}}sid++; } const graph=new Map(),indeg=new Map(),height=new Map();for(const v of vx){graph.set(v,[]);indeg.set(v,0);height.set(v,0);} for(const [a,b] of edges)if(scc.get(a)!==scc.get(b)){graph.get(a).push(b);indeg.set(b,indeg.get(b)+1);} const q=[...vx].filter(v=>indeg.get(v)===0);let best=0; for(let k=0;k600){kh.push(null);continue;} let count=0,oneWay=0;const reach=new Map(); for(const src of vx){const visited=new Set([src]),queue=[src];for(let k=0;kv!==null))s.legacy_krackhardt_mean=mean(kh); else optional.legacy_krackhardt='Partial only: some relation graphs exceeded 600 vertices; no mean reported.'; optional.legacy_path_per_relation=relNames.map((name,i)=>({relation:name,maximum: lengths[i],krackhardt:kh[i]})); } } return {summary:s,optional,topNodes,diverseNodes,relations,nodes, components:wcc.slice().reverse().slice(0,20), distributions:{out_degree:frequencies(out),in_degree:frequencies(inc),distinct_out_relations:frequencies(orValues),unique_out_neighbors:frequencies(onValues)}, parse:{bad_rows:bad},sourceNote:'Measures from complete parsed input, not a sample.', }; } return {add,addNode,output,get rows(){return rows;},get nodes(){return names.length;}}; } const api={createAccumulator,quantile}; if(typeof module==='object'&&module.exports)module.exports=api; if(scope)scope.KGCore=api; })(typeof self!=='undefined'?self:null);