#!/usr/bin/env python3
"""p8v2 k=2 diagnostics, derived from (sample, model file).

Per #hutter_metrics, at k=2 the whole model is bounded three orders of magnitude
below the differences it causes in the trace, so a keep/prune rule cannot be
judged by compression at this k.  These are what it is judged on instead:

  coverage   positions where a kept k=2 rule fires
  hits       of those, where the rule's prediction is right
  removed    positions where k=2 is right and the k=1 table is wrong
             -- trace entries the k=2 layer eliminates
  added      positions where k=2 is wrong and the k=1 table is right
             -- trace entries it creates
  net        removed - added, the layer's worth in trace entries

They are DERIVED, not measured: everything needed is in the model file and the
sample, so they belong to the display step and not to the generated child.
Standard library only.

  python3 tests/pprog/p8v2-diag.py <samplefile> <modelfile>
"""

import sys


def u32(b, off):
    return b[off] | b[off + 1] << 8 | b[off + 2] << 16 | b[off + 3] << 24


def diag(sample_path, model_path):
    mf = open(model_path, "rb").read()
    if len(mf) < 48 or mf[:4] != b"P8V2":
        raise SystemExit("p8v2-diag: %s is not a P8V2 model file" % model_path)
    N, M, TC, TB, G, SC, BWD, ROUNDS = (u32(mf, 4 + 4 * i) for i in range(8))
    axes = mf[36:42].decode("ascii", "replace")
    off = 48
    table = mf[off:off + 256]; off += 256
    off += BWD                      # backward LPP, if this variant stores one
    toksec = mf[off:off + TB]

    tok = {}
    for t in range(TC):
        a, b, c, w = toksec[4 * t:4 * t + 4]
        tok[(a << 8) | b] = (c, w)

    sample = open(sample_path, "rb").read()[:M]
    if len(sample) < M:
        raise SystemExit("p8v2-diag: %s is shorter than the model's M (%d)" % (sample_path, M))

    coverage = hits = removed = added = 0
    for p in range(2, M):
        ctx = (sample[p - 2] << 8) | sample[p - 1]
        e = tok.get(ctx)
        if e is None:
            continue
        coverage += 1
        k2_right = e[0] == sample[p]
        k1_right = table[sample[p - 1]] == sample[p]
        if k2_right:
            hits += 1
            if not k1_right:
                removed += 1
        elif k1_right:
            added += 1
    return dict(M=M, TC=TC, TB=TB, G=G, SC=SC, BWD=BWD, ROUNDS=ROUNDS, axes=axes,
                positions=max(0, M - 2), coverage=coverage, hits=hits,
                removed=removed, added=added, net=removed - added)


if __name__ == "__main__":
    if len(sys.argv) != 3:
        raise SystemExit(__doc__)
    d = diag(sys.argv[1], sys.argv[2])
    print("\t".join("%s=%s" % kv for kv in d.items()))
