forecast.py
Checks: LEARNER: The straddle cover and its readers property; observation; LEARNER: The side loss makes the best reader depend on the stream rule; LEARNER: The stream enters at a reversal theorem; LEARNER: Breaking needs no stream and keeping needs no pole rule.
Cited on: LEARNER.
To run it: save forecast.py, with learner.py, rationed.py beside it; standard library only.
"""forecast.py -- the straddle-cover reader scored on forecasting its
stream: a density forecast collapses to a length loss, a side forecast
makes the best reader depend on the stream, and a reader can depart
from the stream-free optimum only at a REVERSAL.
QUESTION. learner.py's reader commits, at step n, a member C_n of the
straddle cover holding the image J_n, and under every loss monotone in
the committed intervals the greedy reader, which commits the bottom of
J_n, is optimal on every stream at once. Its loss reads the committed
lengths. Score the reader instead on FORECASTING the stream from what
it has committed. Does the best reader then depend on the stream; and
where in the cover can a departure from the greedy reader sit: at a
nested pair of members the loss orders against containment, or at a
choice between incomparable members of one rank, a LAYER, which is
where rationed.py found an income lets the policy in?
THE SLATE. rationed.py's nine rows (learner.py's eight and y -> 2y on
the Fibonacci-word stream), learner.py's 100 policies at no resource
limit, horizon N = 120, counted steps n = 8 .. 119. Two policies whose
committed intervals agree at every counted step on every row are one
BEHAVIOUR CLASS; every loss below is a function of the committed
intervals and the row, so it is constant on a class, and argmin sets
are read over classes. The GREEDY class holds the policies with both
patiences 0; the REFUSER class holds those with both patiences
infinite, which commit the root at every step (learner.py).
THE LOSSES.
The DEFICIT loss is learner.py's: the product of the committed
lengths over the counted steps, infinite when one is unbounded,
infinite losses tying.
The SIDE LOSS. At step n the CUT m_n is the mediant of J_n's two
endpoints, interior to J_n and so to C_n. The stream's image point x
lies inside every image, so which side of m_n it lies on is decided
only by later images: the TRUTH is read by deepening, the first later
image lying wholly on one side of the cut. The reader's FORECAST is
the WIDER side of C_n at the cut, compared exactly; an unbounded
right side is wider; an exact tie forecasts left. The side loss
counts the counted steps whose forecast misses the truth. Under the
identity map the cut is the boundary between the next digit 1 and
the next digit at least 2, so the target is the stream's own next
digit question.
The COMPOSITE order compares side losses and breaks ties by the
deficit.
A ONE-BIT loss scores the side forecast at one counted position j
alone.
A BAND loss. Sort the distinct deficit values over all (class, row)
into POSITIONS 0, 1, ...; a band is an interval [a, b] of positions,
and its loss is 0 when the class's deficit on the row sits in the
band and 1 otherwise, the same band on every row. It reads nothing
but the committed lengths. A band is CLEAN when on every row some
class is in it and some class is out of it. A class's POSITION HULL
is the interval from its least to its greatest position over the
nine rows. A UNION BAND is [p_0, a] together with [b, p_top] for two
positions a < b - 1 that are consecutive in one class's sorted
positions, p_0 and p_top that class's extremes.
THE ARGUMENT (written before the engine).
(A) THE COLLAPSE. Let the forecast at step n be the uniform density
on C_n, and let a score read it only through its density at the
outcome, lower density scoring worse. The outcome x lies in
J_(n+1), inside J_n, inside C_n, so its density is 1/|C_n|
whatever the stream, and the score is a function of |C_n|
increasing in it: a loss monotone in the committed intervals, so
the greedy reader is optimal on every stream by learner.py's
dominance. The log score of the next image, ln|C_n| - ln|J_(n+1)|
summed, is the logarithm of the deficit loss less a sum that no
policy moves. And a forecast read from the image J_n is the same
for every reader. So a score that lets the stream choose the
reader must read the committed interval for something other than
its length: a discrete decision, like the side.
(B) THE REVERSAL. Every sound reader's committed interval holds the
bottom of J_n (learner.py), which the greedy reader commits. So
if on some row a class scores strictly below the greedy class
under a loss, the loss orders two traces against containment,
the lower one's intervals containing the higher one's at every
step: a REVERSAL. Under the side loss, at every step where a
class hits and the greedy class misses, the class's interval
strictly contains the greedy one's. So with no resource limit the
stream moves the optimum off the greedy class only at a reversal,
never inside a layer: two members of one layer are incomparable, and the bottom
is alone in its layer.
(C) THE POLES. A loss that never rises as the intervals shrink keeps
the greedy class in every argmin (dominance). A loss that never
falls as they shrink keeps the refuser in every argmin, since the
root contains every member. For a clean band every row's argmin
is exactly its in-band set, so the nine argmin sets share a class
exactly when some class's nine positions all lie in the band,
that is, when the band contains that class's position hull.
PREDICTIONS (fixed before the engine ran).
P0 [controls, read first] (i) The 100 policies fall into 38
behaviour classes over the nine rows' counted steps. (ii) On the
eight quadratic rows the deepening truth equals the side of the
exact algebraic point at every counted step (the point 2 of the
wall row included), and on the Fibonacci row it resolves at every
counted step within depth 320. (iii) The forecast of the interval
(0, 1) is R at the cut 1/3, L at 2/3 and L at the tie 1/2, and of
(1, infinity) R at the cut 2. (iv) Under the deficit the nine
rows' argmin sets share the greedy class.
P1 [the side loss] The nine argmin sets have no common class, under
the side loss alone and under the composite order. The greedy
class is outside the side argmin on exactly (id, golden),
(id, eights), (sq, golden), (dbl, golden), (dbl, sqrt2) and
(dbl, fib). The least side loss is 0 on the eight quadratic rows
and 47 on (dbl, fib). The nine side argmin sets are pairwise
distinct.
P2 [(B), the reversal] At every (row, step, class) where the class
hits and the greedy class misses, the class's interval strictly
contains the greedy one's. CONTROL: there is at least one such
site, and at least one where a class misses and the greedy class
hits, so the side loss is not monotone either way.
P3 [one bit] For every counted position j, the one-bit loss at j
leaves the nine argmin sets with no common class, alone and with
the deficit breaking its ties.
P4 [(C), the bands] Some clean band has no common class. Revised
after the runs: the hull statement is (C)'s proof and is not run,
a check of it on the position table computing one set statement
twice; the check that did is dropped. Some clean union band has a
common class and none of its common classes is the greedy class
or the refuser.
KILLS (observables).
K0 any P0 control misses: nothing below is read.
K1 P1 prints a common class under either order: the side loss keeps
a stream-free reader on this slate.
K2 a P2 site where the hitting class does not contain the greedy
interval: (B) is false. Its strictness holds by construction, a
different forecast needing a different member; its containment
re-reads learner.py's P4 on the nine rows, the Fibonacci row new.
K3 some position j prints a common class: the side loss's break
needs more than one bit there.
FINDINGS (entered after the run; 15 of 15 checks pass, 14 of 14 once the
hull check was dropped).
F0 The controls pass: 38 classes; the deepening truth agrees with
the exact point at every counted step of the eight quadratic rows
and resolves on the Fibonacci row; the forecast units print as
fixed; under the deficit the greedy class is common to all nine.
The truth strings alternate under constant digits (the tile's
orientation flips each step) and are constant R on (id, sqrt3)
and (sq, golden).
F1 No common class under the side loss or the composite order. The
greedy class is outside the side argmin on exactly the six rows
predicted. The least side loss is 0 on the eight quadratic rows
and 47 of 112 on (dbl, fib); the argmin sets have 2 to 32 of 38
classes and are pairwise distinct over the nine rows.
F2 At 8,018 (row, step, class) sites a class hits where the greedy
class misses, and at every one its interval strictly contains the
greedy one's; at 6,792 sites the greedy class hits where a class
misses. The side loss is monotone in neither direction.
F3 All 112 one-bit losses leave no common class, alone and with the
deficit breaking ties.
F4 117 positions; the greedy class's hull is [0, 85] and the refuser's
[116, 116]. Of 403 clean bands, 146 have no common class, the first
[1, 85]. 213 (class, gap) choices give a clean union band, each
keeping its own seed class common by construction, and in 201 of
them neither pole is common; the first is seeded by the class of
(0, 0, 0, 1), bands [0, 0] and [13, 88], common to the classes of
(0, 0, 0, p), p = 1, 2, 3: tree patience 0 and chain patience 1 to
3.
RUN RECORD. 14 checks, 10.5 s, peak 27 MB. The first run stopped in
the reversal section on an indexing slip (a class's traces read as
one row's); nothing above it changed on the second. Exact integers in
every decision.
"""
import sys
from learner import N0, INF, le, span, inside, run, loss, cmp, policies
from rationed import ROWS9, images9
N_MAIN = 120
N_DEEP = 320
GREEDY = (0, 0, 0, 0)
REFUSER = (0, 0, INF, INF)
# the image point (A + B sqrt(d)) / C of each quadratic stream
POINTS = {"golden": (1, 1, 5, 2), "sqrt2": (0, 1, 2, 1),
"sqrt3": (0, 1, 3, 1), "eights": (-4, 1, 17, 1)}
CHECKS = []
def check(name, ok, detail=""):
CHECKS.append(bool(ok))
print(f" {'PASS' if ok else 'FAIL'} {name}" +
(f" ({detail})" if detail else ""))
def section(title):
print()
print(title)
# the side forecast
def mediant(J):
(a, b), (c, d) = J
return (a + c, b + d)
def forecast(C, cut):
"""'R' when the right side of C at the cut is strictly wider, else
'L'; an unbounded right side is wider."""
lo, hi = span(C)
if hi[1] == 0:
return "R"
# right wider iff lo + hi > 2 cut
lhs = (lo[0] * hi[1] + hi[0] * lo[1]) * cut[1]
rhs = 2 * cut[0] * lo[1] * hi[1]
return "R" if lhs > rhs else "L"
def truth_deep(deep, n):
cut = mediant(deep[n])
for J in deep[n + 1:]:
if le(J[1], cut):
return "L"
if le(cut, J[0]):
return "R"
return None
def image_point(m, s):
A, B, d, C = POINTS[s]
if m == "id":
return A, B, d, C
if m == "dbl":
return 2 * A, 2 * B, d, C
return A * A + B * B * d, 2 * A * B, d, C * C
def truth_exact(point, cut):
"""Side of (A + B sqrt d)/C against p/q, B >= 0; None on equality."""
A, B, d, C = point
p, q = cut
u, w = p * C - A * q, B * q
if u < 0:
return "R"
if w == 0:
return None if u == 0 else "L"
return "R" if u * u < w * w * d else "L"
# the classes and their scores
def build(imgs):
"""Classes over the counted steps: representative policy -> the
per-row committed traces."""
reps = {}
for p in policies():
tr = {row: run(imgs[row], p) for row in ROWS9}
key = tuple(tuple(tr[row][N0:]) for row in ROWS9)
if key not in reps:
reps[key] = (p, [p], tr)
else:
reps[key][1].append(p)
return [(p, members, tr) for p, members, tr in reps.values()]
def class_of(classes, pol):
for i, (_, members, _) in enumerate(classes):
if pol in members:
return i
raise AssertionError("policy in no class")
def argmin_by(values, key_cmp):
"""Indices whose value is least under key_cmp."""
best = [0]
for i in range(1, len(values)):
c = key_cmp(values[i], values[best[0]])
if c < 0:
best = [i]
elif c == 0:
best.append(i)
return set(best)
def common(sets):
out = None
for s in sets:
out = set(s) if out is None else out & s
return out
def int_cmp(x, y):
return (x > y) - (x < y)
def composite_cmp(x, y):
c = int_cmp(x[0], y[0])
return c if c else cmp(x[1], y[1])
# the sections
def section_controls(imgs, deep, classes, truths):
section("P0 CONTROLS")
check("38 behaviour classes among the 100 policies",
len(classes) == 38, f"{len(classes)} classes")
bad = unresolved = 0
for m, s in ROWS9:
for n in range(N0, N_MAIN):
if truths[(m, s)][n - N0] is None:
unresolved += 1
elif s != "fib":
ex = truth_exact(image_point(m, s), mediant(deep[(m, s)][n]))
bad += ex != truths[(m, s)][n - N0]
check("deepening truth equals the exact point's side on 8 rows",
bad == 0 and unresolved == 0,
f"{bad} disagreements, {unresolved} unresolved")
for (m, s) in ROWS9:
print(f" truth {m + '/' + s:12s} " + "".join(truths[(m, s)]))
unit = (0, 1), (1, 1)
ok = (forecast((unit[0], unit[1], 0, 0), (1, 3)) == "R" and
forecast((unit[0], unit[1], 0, 0), (2, 3)) == "L" and
forecast((unit[0], unit[1], 0, 0), (1, 2)) == "L" and
forecast(((1, 1), (1, 0), 0, 0), (2, 1)) == "R")
check("forecast units: (0,1) at 1/3, 2/3, 1/2; (1,inf) at 2", ok)
g = class_of(classes, GREEDY)
sets = [argmin_by([loss(tr[row]) for _, _, tr in classes], cmp)
for row in ROWS9]
check("under the deficit the greedy class is common to all nine",
g in common(sets), f"common {sorted(common(sets))}")
def side_hits(classes, deep, truths):
"""hits[i][row] = list of 0/1 per counted step."""
out = []
for _, _, tr in classes:
per = {}
for row in ROWS9:
per[row] = [int(forecast(tr[row][n], mediant(deep[row][n])) ==
truths[row][n - N0])
for n in range(N0, N_MAIN)]
out.append(per)
return out
def section_side(classes, hits):
section("P1 THE SIDE LOSS")
g = class_of(classes, GREEDY)
k = len(classes)
pure_sets, comp_sets = [], []
for row in ROWS9:
miss = [len(hits[i][row]) - sum(hits[i][row]) for i in range(k)]
vals = [(miss[i], loss(classes[i][2][row])) for i in range(k)]
ps = argmin_by(miss, int_cmp)
cs = argmin_by(vals, composite_cmp)
pure_sets.append(ps)
comp_sets.append(cs)
print(f" {row[0] + '/' + row[1]:12s} least {min(miss):3d} "
f"most {max(miss):3d} argmin {len(ps):2d} (composite "
f"{len(cs):2d}) greedy {'in' if g in ps else 'OUT'}")
check("no common class under the side loss",
not common(pure_sets), f"{sorted(common(pure_sets))}")
check("no common class under the composite order",
not common(comp_sets), f"{sorted(common(comp_sets))}")
out_rows = {row for row, s in zip(ROWS9, pure_sets) if g not in s}
want = {("id", "golden"), ("id", "eights"), ("sq", "golden"),
("dbl", "golden"), ("dbl", "sqrt2"), ("dbl", "fib")}
check("greedy outside the side argmin on exactly the six rows",
out_rows == want, f"{sorted(out_rows)}")
least = {row: min(len(hits[i][row]) - sum(hits[i][row])
for i in range(k)) for row in ROWS9}
check("least side loss 0 on eight rows, 47 on (dbl, fib)",
all(least[r] == 0 for r in ROWS9 if r[1] != "fib") and
least[("dbl", "fib")] == 47, f"fib {least[('dbl', 'fib')]}")
distinct = len({frozenset(s) for s in pure_sets})
check("the nine side argmin sets are pairwise distinct",
distinct == 9, f"{distinct} distinct sets")
def section_reversal(classes, hits):
section("P2 THE REVERSAL")
g = class_of(classes, GREEDY)
gain = broken = loss_sites = 0
for row in ROWS9:
gtr = classes[g][2][row]
for i, (_, _, trs) in enumerate(classes):
tr = trs[row]
for j, n in enumerate(range(N0, N_MAIN)):
if hits[i][row][j] and not hits[g][row][j]:
gain += 1
if not (inside(gtr[n], tr[n]) and gtr[n] != tr[n]):
broken += 1
if hits[g][row][j] and not hits[i][row][j]:
loss_sites += 1
check("every class hitting where greedy misses strictly contains it",
broken == 0 and gain > 0,
f"{gain} sites, {broken} not containing")
check("and greedy also hits where some class misses",
loss_sites > 0, f"{loss_sites} sites")
def section_one_bit(classes, hits):
section("P3 ONE BIT")
k = len(classes)
worst_pure = worst_comp = 0
for j in range(N_MAIN - N0):
ps, cs = [], []
for row in ROWS9:
miss = [1 - hits[i][row][j] for i in range(k)]
ps.append(argmin_by(miss, int_cmp))
cs.append(argmin_by([(miss[i], loss(classes[i][2][row]))
for i in range(k)], composite_cmp))
worst_pure += bool(common(ps))
worst_comp += bool(common(cs))
check("every position's one-bit loss leaves no common class",
worst_pure == 0 and worst_comp == 0,
f"{worst_pure} positions keep alone, {worst_comp} with deficit")
def section_bands(classes):
section("P4 THE BANDS")
k = len(classes)
g = class_of(classes, GREEDY)
rf = class_of(classes, REFUSER)
vals = {(i, row): loss(classes[i][2][row])
for i in range(k) for row in ROWS9}
# positions: distinct values in the deficit order (infinite ties)
order = []
for v in vals.values():
if not any(cmp(v, w) == 0 for w in order):
order.append(v)
# insertion sort under cmp, exact
srt = []
for v in order:
at = len(srt)
for t, w in enumerate(srt):
if cmp(v, w) < 0:
at = t
break
srt.insert(at, v)
pos = {}
for key, v in vals.items():
pos[key] = next(t for t, w in enumerate(srt) if cmp(v, w) == 0)
P = len(srt)
hull = [(min(pos[(i, r)] for r in ROWS9), max(pos[(i, r)] for r in ROWS9))
for i in range(k)]
print(f" {P} positions; greedy hull {hull[g]}, refuser hull {hull[rf]}")
def in_sets(member):
return [{i for i in range(k) if member(pos[(i, row)])}
for row in ROWS9]
clean = breakers = 0
first = None
for a in range(P):
for b in range(a, P):
sets = in_sets(lambda x: a <= x <= b)
if not all(0 < len(s) < k for s in sets):
continue
clean += 1
keep = bool(common(sets))
if not keep:
breakers += 1
first = first or (a, b)
check("some clean band has no common class", breakers > 0,
f"{clean} clean bands, {breakers} breakers, first {first}")
found = poleless = 0
specimen = None
for c in range(k):
ps = sorted({pos[(c, r)] for r in ROWS9})
for a, b in zip(ps, ps[1:]):
if b - a < 2:
continue
lo, hi = ps[0], ps[-1]
sets = in_sets(lambda x: lo <= x <= a or b <= x <= hi)
if not all(0 < len(s) < k for s in sets):
continue
com = common(sets)
found += 1
if g not in com and rf not in com:
poleless += 1
specimen = specimen or (c, (lo, a), (b, hi), sorted(com))
check("a clean union band keeps with neither pole common",
poleless > 0, f"{found} clean union bands, each keeping its "
f"seed class, {poleless} poleless")
if specimen:
c, b1, b2, com = specimen
print(f" specimen: class {c} ({classes[c][0]}), bands {b1} "
f"and {b2}, common {com}")
print(f" members: " + ", ".join(str(classes[i][0]) for i in com))
def main():
imgs = images9(N_MAIN)
deep = images9(N_DEEP)
classes = build(imgs)
truths = {row: [truth_deep(deep[row], n) for n in range(N0, N_MAIN)]
for row in ROWS9}
section_controls(imgs, deep, classes, truths)
if not all(CHECKS):
print("\nA CONTROL FAILED: nothing below is read.")
sys.exit(1)
hits = side_hits(classes, deep, truths)
section_side(classes, hits)
section_reversal(classes, hits)
section_one_bit(classes, hits)
section_bands(classes)
print()
print(f"{sum(CHECKS)} of {len(CHECKS)} checks pass")
sys.exit(0 if all(CHECKS) else 1)
if __name__ == "__main__":
main()