sublattice.py

Checks: LATENCY: Trees of units property; rule; observation.

Cited on: LATENCY.

To run it: save sublattice.py, with chains.py, dags.py, graphs.py, manystream.py, poles.py, redundant.py beside it; standard library only.

"""
sublattice.py -- is a graph's feasible set a lattice on its shared units?

QUESTION. ors.py: at (6, 2, 4) a cone reading pinned heads has, at each
latency T, a sublattice of head settings. Over dags.py's shared classes
(2..5 units at the 34 SQ cells, 2..4 at the 441), at each T from
brute's least to naive: F_T, the assignments in brute's window (L_j in
0..L*_j + 3) with every unit alive and latency <= T, projected onto the
SHARED units (two or more readers), the other units existentially
optimized. Is F_T closed under componentwise max (min)? If max-closed
(in some coordinates), the greatest feasible setting is found by
propagation and the graph is priced in polynomial time.

COORDINATES. D, the shared units' delays; A, their arrivals. A head
(fresh inputs only) has arrival = delay, so A = D wherever only heads
are shared.

BY HAND (frozen before code). Only heads shared: the other units form
cones rooted at sinks over fresh streams and heads, F_T the
intersection of their head tables, a head read twice in one cone the
diagonal of a two-mark table; by ors.py each is a sublattice at
(6, 2, 4) within its sizes, so closed both ways there (elsewhere a
TRANSPLANT). A shared unit above a shared unit: in D the path sum
L_h + L_g <= c holds at (2, 3) and (3, 2), not at (3, 3), so D can
fail by it alone. In A the latency bound is unary and g's own
relations have the form a_g >= phi(a_h), max-closed whatever phi; a
reader of h and g spends budget as a_g rises while gaining inset, so A
can fail through the projection.

KILL (frozen): "A not max-closed" > 0 over the (class, cell, T) with a
shared non-head; predicted 0, weakly. D predicted closed where only
heads are shared, "D not max-closed" > 0 where a shared unit sits above
another.

Frozen after reading the kill: F arm, the arrivals of EVERY unit (no
projection), "F not min-closed" > 0, predicted to fire weakly (an
internal unit's delay trades against its reader's along one path);
and where A fails max-closure, its cells; then the testable
count and A's testable cells. Y5 reads exactly 5 units, so no triple
is counted twice.

CONTROLS. C1 the enumerator: its least latency = dags.brute's, and its
alive set at latency <= naive = an itertools.product filter over the
full window, every class of 2..4 units at (6, 2, 4) and (8, 3, 5),
and of 5 at (6, 2, 4). C2
the detector on synthetic sets. C3 the hand witness h, c = (z, h),
r = (h, c) at (6, 2, 4), T = 6: F_6 = {2, 3} on h's delay.

FINDINGS (copied from the run below). C1 148 (class, cell) pairs, 0
off; C2, C3 green. Y4 (17 classes of 2..4 units, 441 cells) and Y5
(the 114 of exactly 5, 34 SQ cells): 13,900 (class, cell, T) triples.
Added after reading the kill: a projection can fail closure only where
two of its points are incomparable (TESTABLE), and A's testable cells.
  Heads only (5,569 + 2,442; testable 12 + 69 in both arms, Y4's at 12
  cells none of them (6, 2, 4), Y5's at 28): closed under min and max
  at every triple, as predicted at (6, 2, 4), observed at the others.
  A shared non-head, none above another (941 + 700; testable 0 + 26,
  one at each of 26 cells): closed both ways, both arms.
  A shared unit above another (1,407 + 2,841): D fails max-closure at
  every testable triple (92 of 92, 1,105 of 1,105), min at 6 + 4, the
  first met at (4, 2, 2), h, c = (z, h), (z, c), (h, c), T = 5, delays
  (3, 1) and (2, 2) alive, (3, 2) not: the path sum, as predicted. A
  is testable at 0 + 70 and fails max at 4, all at (6, 2, 4):
  KILL FIRES (predicted 0). The first met: h, c = (z, h), r = (h, c),
  (z, r) twice, T = 8, arrivals (h, r) = (2, 7) and (3, 6) alive,
  (3, 7) not. A is min-closed at every triple (testable 12 + 165).
  F, every unit's arrival: not max-closed at 1 + 10, not min-closed at
  0 + 9 (testable 254 + 1,867), as predicted.
So a greatest-element pass in arrivals is dead at (6, 2, 4); a least
one is not refuted, on 177 testable triples.

RUN RECORD. main: 3/3 checks, 32.8 s, peak commit 9.4 MB.
"""
import itertools
import sys
import time
from collections import Counter

import chains
import dags
import graphs

CHECKS = []


def check(name, ok, detail=""):
    CHECKS.append(bool(ok))
    tail = f"  ({detail})" if detail else ""
    print(f"  {'PASS' if ok else 'FAIL'}  {name}{tail}")


def alive_all(d, cap):
    """Every assignment in brute's window, every unit alive, latency <=
    cap: a list of (latency, delays, arrivals)."""
    out = []
    left, arr, Ls = [None] * d.n, [0] * d.n, [0] * d.n

    def go(j):
        if j == d.n:
            out.append((max(arr), tuple(Ls), tuple(arr)))
            return
        for L in range(0, d.Ls[j] + 4):
            a = d.arrival(j, L, arr)
            if a > cap:
                break
            r = d.leftover(j, L, left)
            if r is None:
                continue
            left[j], arr[j], Ls[j] = r, a, L
            go(j + 1)
        left[j], arr[j] = None, 0

    go(0)
    return out


def alive_product(d, cap):
    out = []
    for Ls in itertools.product(*[range(0, x + 4) for x in d.Ls]):
        lat, ok = d.evaluate(list(Ls))
        if ok and lat <= cap:
            arr = [0] * d.n
            for j in range(d.n):
                arr[j] = d.arrival(j, Ls[j], arr)
            out.append((lat, tuple(Ls), tuple(arr)))
    return out


def closed(P):
    """(min-closed, max-closed, a max witness (p, q) or None,
    testable: some pair incomparable, so closure can fail at all)."""
    lo, hi, wit, inc = True, True, None, False
    P = set(P)
    for p in P:
        for q in P:
            if not (all(a <= b for a, b in zip(p, q))
                    or all(a >= b for a, b in zip(p, q))):
                inc = True
            if tuple(map(min, p, q)) not in P:
                lo = False
            if tuple(map(max, p, q)) not in P:
                if hi:
                    wit = (p, q)
                hi = False
    return lo, hi, wit, inc


def shared_units(d):
    return [j for j in range(d.n) if len(d.readers[j]) >= 2]


def is_head(d, j):
    return d.units[j] == (None, None)


def above(d, g, h):
    """g reads h, directly or through other units."""
    stack = [x for x in d.units[g] if x is not None]
    seen = set()
    while stack:
        x = stack.pop()
        if x == h:
            return True
        if x in seen:
            continue
        seen.add(x)
        stack += [y for y in d.units[x] if y is not None]
    return False


def kind(d, S):
    if all(is_head(d, j) for j in S):
        return "heads only"
    if any(above(d, g, h) for g in S for h in S if g != h):
        return "shared above shared"
    return "shared non-head"


def read_pair(cell, units):
    """[(T, kind, D closure, A closure)] for T = least..naive."""
    d = dags.Dag(cell, units)
    nv = d.naive()
    rows = alive_all(d, nv)
    least = min(r[0] for r in rows)
    S = shared_units(d)
    k = kind(d, S)
    out = []
    for T in range(least, nv + 1):
        PD = {tuple(r[1][j] for j in S) for r in rows if r[0] <= T}
        PA = {tuple(r[2][j] for j in S) for r in rows if r[0] <= T}
        PF = {r[2] for r in rows if r[0] <= T}
        out.append((T, k, closed(PD), closed(PA), closed(PF)))
    return d, least, out


def controls():
    print("C1..C3  the instrument")
    off, cnt = 0, 0
    pop = [(c, u) for c in (chains.ms.Cell(6, 2, 4), chains.ms.Cell(8, 3, 5))
           for u in dags.classes(4)]
    pop += [(chains.ms.Cell(6, 2, 4), u) for u in dags.classes(5)
            if len(u) == 5]
    for cell, units in pop:
        d = dags.Dag(cell, units)
        nv = d.naive()
        a = alive_all(d, nv)
        b = alive_product(d, nv)
        cnt += 1
        off += sorted(a) != sorted(b)
        off += min(r[0] for r in a) != dags.brute(d)[0]
    check("C1 enumerator = product filter, least = brute (2..4 units at "
          "two cells, 5 at (6, 2, 4))", off == 0,
          f"{cnt} (class, cell) pairs, {off} off")
    syn = {"cross": {(1, 2), (2, 1)},
           "square": {(1, 2), (2, 1), (2, 2), (1, 1)},
           "chain3": {(1, 1), (2, 2), (1, 2)}}
    got = {k: closed(v)[:2] for k, v in syn.items()}
    check("C2 detector: cross neither, square both, chain3 both", got == {
        "cross": (False, False), "square": (True, True),
        "chain3": (True, True)}, f"{got}")
    cell = chains.ms.Cell(6, 2, 4)
    w = ((None, None), (None, 0), (0, 1))
    d = dags.Dag(cell, w)
    rows = alive_all(d, 6)
    F = sorted({(r[1][0],) for r in rows if r[0] <= 6})
    check("C3 the hand witness at T = 6: h's delays {2, 3}",
          F == [(2,), (3,)], f"{F}, shared {shared_units(d)}")


def sweep(tag, nmin, nmax, cells_):
    t0 = time.time()
    cls = [u for u in dags.classes(nmax) if len(u) >= nmin]
    n = Counter()
    first = {}
    for cell in cells_:
        for units in cls:
            d, least, out = read_pair(cell, units)
            for T, k, cd, ca, cf in out:
                n[(k, "triples")] += 1
                if not ca[1]:
                    n[(k, f"A max fails at {graphs.name(cell)}")] += 1
                for arm, c in (("D", cd), ("A", ca), ("F", cf)):
                    n[(k, f"{arm} testable")] += c[3]
                    if c[3] and arm == "A":
                        n[(k, f"A testable at {graphs.name(cell)}")] += 1
                    if not c[1]:
                        n[(k, f"{arm} not max-closed")] += 1
                        key = (k, arm, "max")
                        if key not in first:
                            first[key] = (graphs.name(cell), units, T,
                                          shared_units(d), c[2])
                    if not c[0]:
                        n[(k, f"{arm} not min-closed")] += 1
                        key = (k, arm, "min")
                        if key not in first:
                            first[key] = (graphs.name(cell), units, T,
                                          shared_units(d))
    print(f"\n{tag}  {len(cls)} shared classes of {nmin}..{nmax} units at "
          f"{len(cells_)} cells, {time.time() - t0:.1f} s")
    for k in ("heads only", "shared non-head", "shared above shared"):
        row = {s: n[(k, s)] for s in ("triples", "D testable",
                                      "A testable", "F testable",
                                      "D not max-closed",
                                      "D not min-closed", "A not max-closed",
                                      "A not min-closed", "F not max-closed",
                                      "F not min-closed")}
        print(f"    {k:20s} {row}")
        where = {s: v for (kk, s), v in n.items()
                 if kk == k and (s.startswith("A max fails")
                                 or s.startswith("A testable at"))}
        if where:
            print(f"    {'':20s} {where}")
    for key in sorted(first):
        print(f"    first {key}: {first[key]}")
    return n


def main():
    t0 = time.time()
    controls()
    if not all(CHECKS):
        print("\nCONTROL FAILED: nothing below is read.")
        return 1
    cs = list(graphs.cells())
    gain = lambda c, k: (lambda p: p[1] < p[0])(graphs.price(
        c, graphs.SHAPES[k]))
    sq = [c for c in cs if graphs.name(c) in ("(6, 2, 4)", "(35, 8, 27)")
          or gain(c, "S") or gain(c, "Q")]
    print(f"SQ cells: {len(sq)}")
    n4 = sweep("Y4", 2, 4, cs)
    n5 = sweep("Y5", 5, 5, sq)
    kill = sum(n[(k, "A not max-closed")] for n in (n4, n5)
               for k in ("shared non-head", "shared above shared"))
    print(f"\nKILL A not max-closed over shared non-heads: {kill}")
    print(f"\n{sum(CHECKS)}/{len(CHECKS)} checks, {time.time() - t0:.1f} s")
    return 0


if __name__ == "__main__":
    sys.exit(main())