#!/usr/bin/env python3
# Differential check: our port's state dump vs a G0 (Linas) state dump, over the (atom, key) pairs the port
# claims to reproduce. Counts must match exactly; other values to a relative 1e-9.
# diffstate.py <linas-state.tsv> <ours.tsv> exit 0 = identical on the ported set
import math, re, sys
PORTED = {
'word': {'*-TruthValueKey-*'},
'pair': {'*-TruthValueKey-*', '*-FrequencyKey-*', '*-Mutual Info Key-*'},
'marg': {'*-Norm Key-*', '*-FrequencyKey-*', '*-Entropy Key-*', '*-Mutual Info Key-*'},
'total': {'*-Left Total Key-*', '*-Right Total Key-*', '*-Supp Dimension Key-*', '*-Total Entropy Key-*', '*-Total MI Key-*'},
'section': {'*-TruthValueKey-*'},
'bond': {'*-TruthValueKey-*'},
}
# --counts-only: compare only TruthValueKey counts, on words, ANY pairs, named-bond pairs and Sections.
COUNTS_ONLY = '--counts-only' in sys.argv
if COUNTS_ONLY: sys.argv.remove('--counts-only')
BOND = re.compile(r'^\(EdgeLink \(BondNode "([^"]*)"\) \(ListLink \(WordNode "[^"]*"\) \(WordNode "[^"]*"\)\)\)$')
EDGE = re.compile(r'^\(EdgeLink \(BondNode "ANY"\) \(ListLink \((WordNode|AnyNode) "[^"]*"\) \((WordNode|AnyNode) "[^"]*"\)\)\)$')
def kind(atom):
if atom.startswith('(WordNode "'): return 'word'
if atom.startswith('(Section (WordNode "'): return 'section'
b = BOND.match(atom)
if b and b.group(1) != 'ANY': return 'bond'
m = EDGE.match(atom)
if not m: return None
a, b = m.group(1), m.group(2)
return {('WordNode', 'WordNode'): 'pair', ('AnyNode', 'AnyNode'): 'total'}.get((a, b), 'marg')
def num(s):
return {'+inf.0': math.inf, '-inf.0': -math.inf, 'inf': math.inf, '-inf': -math.inf}.get(s, None) if s in ('+inf.0', '-inf.0', 'inf', '-inf') else float(s)
def load(path):
d = {}
for line in open(path):
parts = line.rstrip('\n').split('\t')
if len(parts) != 3: continue
atom, key, vals = parts
k = kind(atom)
m = re.match(r'^\(PredicateNode "(.*)"\)$', key)
if not k or not m or m.group(1) not in PORTED[k]: continue
if COUNTS_ONLY and m.group(1) != '*-TruthValueKey-*': continue
if not COUNTS_ONLY and k in ('section', 'bond'): continue
d[(atom, m.group(1))] = [num(x) for x in vals.split()]
return d
def same(a, b, exact):
if len(a) != len(b): return False
for x, y in zip(a, b):
if math.isinf(x) or math.isinf(y):
if x != y: return False
elif exact:
if x != y: return False
elif abs(x - y) > 1e-9 * max(abs(x), abs(y), 1e-300) and abs(x - y) > 1e-15:
return False
return True
L, O = load(sys.argv[1]), load(sys.argv[2])
only_l = sorted(set(L) - set(O)); only_o = sorted(set(O) - set(L))
bad = [k for k in sorted(set(L) & set(O)) if not same(L[k], O[k], k[1] == '*-TruthValueKey-*')]
by = {}
for k in set(L) & set(O): by[k[1]] = by.get(k[1], 0) + 1
print(f'linas {len(L)} ours {len(O)} both {len(set(L) & set(O))} only-linas {len(only_l)} only-ours {len(only_o)} value-mismatch {len(bad)}')
for key, n in sorted(by.items()): print(f' {key}: {n}')
for k in only_l[:5]: print('ONLY-LINAS', k)
for k in only_o[:5]: print('ONLY-OURS', k)
for k in bad[:8]: print('MISMATCH', k, L[k], O[k])
sys.exit(0 if not (only_l or only_o or bad) else 1)