"""TEN DAYS — 八時間を十日 Ten sessions of eight machine-hours on one stainless plate. Eighty hours, 7200 metres, one tool that is never sharpened and one sheet that is never replaced. What carries over and what does not ----------------------------------- The plate carries over. Every groove cut on day one is still there on day ten and is part of what day ten has to read. The machine does not carry over. At the end of a session it is powered down, and the weights of its predictor go with it. Each morning it wakes with no model at all and has to learn the surface again from nothing — except that the surface it wakes to is the accumulated record of every previous day, including its own mistakes. The stylus is left where it stopped. Nothing is re-homed. This gives a measurement the single session could not: morning surprise. If it falls from day to day while the model is being reset every morning, then what got easier was not the machine. It was the world. Every rule of motion is byte-identical to scribe.py. Nothing about how the line decides was changed for this run, so the two can be compared directly. Usage: python3 scribe_days.py --days 10 --hours 8 --out ../plate/ten_days """ from __future__ import annotations import argparse import json import math import os import time import numpy as np from scribe import (Metal, PLATE_W, PLATE_H, GROOVE, SCRIBE_MM_S, CELL, MARGIN, DS, LOOKAHEAD, PROBE_SPREAD, SENSE_R, GAIN_GRADIENT, GAIN_SURPRISE, GAIN_EDGE, LEARN_RATE, TURN_CLAMP) MORNING_STEPS = 60000 # first ~36 m of a session: what waking up costs def run_session(metal, x, y, th, steps, log_every): """One eight-hour session. The predictor starts empty; the plate does not.""" w = [0.0] * 6 # the machine wakes with no model bias = 0.0 # the morning/evening windows must not overlap, or a short session would # report the same number twice and divide it by the wrong length win = max(1, min(MORNING_STEPS, steps // 4)) pts = [(x, y)] trace = [] surprise_run = 0.0 turn_prev = 0.0 cut_mm = straight_mm = 0.0 surprise_sum = 0.0 morning_sum = 0.0 evening_sum = 0.0 for step in range(steps): ax, ay = x + LOOKAHEAD * math.cos(th), y + LOOKAHEAD * math.sin(th) lx = x + LOOKAHEAD * math.cos(th - PROBE_SPREAD) ly = y + LOOKAHEAD * math.sin(th - PROBE_SPREAD) rx = x + LOOKAHEAD * math.cos(th + PROBE_SPREAD) ry = y + LOOKAHEAD * math.sin(th + PROBE_SPREAD) d_here = metal.density(x, y) d_left = metal.density(lx, ly) d_right = metal.density(rx, ry) cov = metal.coverage() feats = [d_here, d_left, d_right, abs(turn_prev), cov, surprise_run] pred = bias + sum(wi * fi for wi, fi in zip(w, feats)) actual = metal.density(ax, ay) err = actual - pred ae = abs(err) for i, fi in enumerate(feats): w[i] += LEARN_RATE * err * fi bias += LEARN_RATE * err * 0.25 surprise_run = 0.98 * surprise_run + 0.02 * ae surprise_sum += ae if step < win: morning_sum += ae if step >= steps - win: evening_sum += ae drive = math.tanh((d_left - d_right) * 6.0) turn = (GAIN_GRADIENT + GAIN_SURPRISE * ae) * drive ex = min(x - MARGIN, PLATE_W - MARGIN - x) ey = min(y - MARGIN, PLATE_H - MARGIN - y) edge = min(ex, ey) if edge < 26.0: cx, cy = PLATE_W * 0.5, PLATE_H * 0.5 to_c = math.atan2(cy - y, cx - x) diff = math.atan2(math.sin(to_c - th), math.cos(to_c - th)) turn += GAIN_EDGE * diff * (1.0 - max(0.0, edge) / 26.0) turn = max(-TURN_CLAMP, min(TURN_CLAMP, turn)) th += turn turn_prev = turn nx, ny = x + DS * math.cos(th), y + DS * math.sin(th) nx = min(PLATE_W - 1.0, max(1.0, nx)) ny = min(PLATE_H - 1.0, max(1.0, ny)) seg = math.hypot(nx - x, ny - y) k = max(1, int(seg / (CELL * 0.5))) for j in range(1, k + 1): t = j / k metal.cut(x + (nx - x) * t, y + (ny - y) * t) cut_mm += seg if abs(turn) < 0.02: straight_mm += seg x, y = nx, ny pts.append((x, y)) if step % log_every == 0: trace.append({ "step": step, "mm": round(cut_mm, 1), "coverage": round(metal.coverage(), 5), "surprise": round(ae, 5), "surprise_avg": round(surprise_run, 5), "turn": round(turn, 5), "pred": round(pred, 4), "actual": round(actual, 4), }) stats = { "cut_mm": round(cut_mm, 1), "straight_fraction": round(straight_mm / max(1e-9, cut_mm), 4), "mean_surprise": round(surprise_sum / max(1, steps), 6), "morning_surprise": round(morning_sum / win, 6), "evening_surprise": round(evening_sum / win, 6), "window_steps": win, "final_weights": [round(v, 4) for v in w], "coverage_after": round(metal.coverage(), 5), } return pts, trace, stats, x, y, th def thin(pts, min_d2=0.09): out = [pts[0]] for p in pts[1:]: q = out[-1] if (p[0] - q[0]) ** 2 + (p[1] - q[1]) ** 2 >= min_d2: out.append(p) return out def main(): ap = argparse.ArgumentParser() ap.add_argument("--days", type=int, default=10) ap.add_argument("--hours", type=float, default=8.0) ap.add_argument("--out", default="../plate/ten_days") ap.add_argument("--log-every", type=int, default=4000) args = ap.parse_args() out = os.path.abspath(args.out) os.makedirs(out, exist_ok=True) metal = Metal() x, y = PLATE_W * 0.5, PLATE_H * 0.5 th = 0.6 steps = int(args.hours * 3600.0 * SCRIBE_MM_S / DS) days = [] all_pts = [] wall0 = time.time() for day in range(1, args.days + 1): cov_before = metal.coverage() t0 = time.time() pts, trace, st, x, y, th = run_session(metal, x, y, th, steps, args.log_every) st.update({ "day": day, "coverage_before": round(cov_before, 5), "machine_hours": args.hours, "wall_s": round(time.time() - t0, 1), }) days.append(st) tp = thin(pts) all_pts.extend(tp if not all_pts else tp[1:]) with open(os.path.join(out, "day%02d_stroke.json" % day), "w") as f: json.dump({"pts": [[round(p[0], 2), round(p[1], 2)] for p in tp]}, f) with open(os.path.join(out, "day%02d_trace.json" % day), "w") as f: json.dump(trace, f) with open(os.path.join(out, "days.json"), "w") as f: json.dump({"days": days, "in_progress": day < args.days}, f, ensure_ascii=False, indent=1) np.save(os.path.join(out, "metal.npy"), np.frombuffer(bytes(metal.g), dtype=np.uint8).copy()) print("day %2d: %.1f m cut, coverage %.3f -> %.3f, morning %.5f, evening %.5f, " "straight %.1f%%, %.0fs" % (day, st["cut_mm"] / 1000, cov_before, st["coverage_after"], st["morning_surprise"], st["evening_surprise"], 100 * st["straight_fraction"], st["wall_s"]), flush=True) with open(os.path.join(out, "stroke.json"), "w") as f: json.dump({"pts": [[round(p[0], 2), round(p[1], 2)] for p in all_pts]}, f) summary = { "days": args.days, "hours_per_day": args.hours, "machine_hours_total": args.days * args.hours, "cut_m_total": round(sum(d["cut_mm"] for d in days) / 1000.0, 1), "coverage_final": days[-1]["coverage_after"], "morning_surprise_day1": days[0]["morning_surprise"], "morning_surprise_last": days[-1]["morning_surprise"], "straight_fraction_day1": days[0]["straight_fraction"], "straight_fraction_last": days[-1]["straight_fraction"], "points_stored": len(all_pts), "plate_mm": [PLATE_W, PLATE_H], "groove_mm": GROOVE, "wall_s": round(time.time() - wall0, 1), "note": ("The plate persists across sessions; the predictor is reset to zero " "every morning. Morning surprise therefore measures the world, not " "the model."), } with open(os.path.join(out, "summary.json"), "w") as f: json.dump(summary, f, ensure_ascii=False, indent=1) print(json.dumps(summary, ensure_ascii=False)) if __name__ == "__main__": main()