An implementation of indivisible stochastic processes where the process is keyboard mashing
0

Configure Feed

Select the types of activity you want to include in your feed.

MashModeler / ispKeyboard.py
15 kB 382 lines
1#!/usr/bin/env python3 2""" 3Keyboard-Driven Indivisible Stochastic Process 4================================================ 5Based on Jacob Barandes (arXiv:2507.21192, 2302.10778). 6 7Formal mapping 8-------------- 9 C : configuration space — the set of known keys (grows on first press) 10 eⱼ : delta spike on key j — the state at a division event (we observed j) 11 Γ(t←t₀) : N×N column-stochastic transition matrix — the nomological law 12 updated from bigram history after every division event 13 p(t) : Γ · eⱼ = column j of Γ — the predicted distribution over the 14 NEXT keypress, given key j was just pressed; this is where all 15 memory of the process lives (encoded in Γ, not in p(t₀)) 16 17Every keypress is a division event: 18 1. Observe key j → p(t₀) = eⱼ (delta spike, t₀ resets) 19 2. Update Γ from the new (prev→j) bigram observation 20 3. p(t) = column j of updated Γ → predicted distribution over next key 21 22Indivisibility: Γ is a primitive law between consecutive division events. 23It is NOT required to factor as a product of sub-step matrices. 24 25Controls: any printable key → division event (immediate) 26 BACKSPACE → undo last keypress 27 ESC / Ctrl-C → quit 28""" 29 30import sys, os, tty, termios, fcntl, re 31import numpy as np 32from typing import Optional 33 34# ── ANSI ───────────────────────────────────────────────────────────────────── 35RS = "\033[0m" 36BLD = "\033[1m" 37DIM = "\033[2m" 38CYN = "\033[96m" 39GRN = "\033[92m" 40YLW = "\033[93m" 41RED = "\033[91m" 42MAG = "\033[95m" 43 44def col(text, *codes): 45 return "".join(codes) + str(text) + RS 46 47_ANSI = re.compile(r'\033\[[0-9;]*m') 48def vlen(s): 49 return len(_ANSI.sub('', s)) 50 51 52# ── ISP model ───────────────────────────────────────────────────────────────── 53 54class KeyboardISP: 55 """ 56 Indivisible stochastic process over a keyboard configuration space. 57 58 State 59 ----- 60 configs : ordered list of known keys — the configuration space C 61 gamma : N×N column-stochastic matrix — Γ(t←t₀), the nomological law 62 p : N-vector — current predicted distribution over next keypress 63 = column of Γ for the last observed key 64 = Γ · e_last (law of total probability, §2.2 eq.20) 65 last_key : the key observed at the most recent division event (t₀) 66 67 Every keypress is a division event: 68 - p(t₀) collapses to a delta spike on the observed key 69 - Γ is updated from the new bigram observation 70 - p(t) = Γ · e_j (column j of updated Γ) 71 """ 72 PRIOR = 0.5 # Laplace / Krichevsky-Trofimov smoothing 73 74 def __init__(self): 75 self.configs = [] # C: ordered list of keys 76 self.index = {} # key -> matrix index 77 self.counts = np.zeros((0, 0)) # bigram counts[src_idx, dst_idx] 78 self.gamma = None # Γ: current transition matrix 79 self.p = None # predicted distribution over next key 80 self.last_key = None # key at last division event (t₀) 81 self.prev_key = None # key at division event before that 82 self.history = [] # full sequence of observed keys 83 self.n_events = 0 # total division events 84 self.last_save_path = None # most recent save path 85 86 87 88 # ── configuration space ─────────────────────────────────────────────── 89 90 def _expand(self, key): 91 """Add a new key to C, growing Γ by one row and column.""" 92 n = len(self.configs) 93 self.configs.append(key) 94 self.index[key] = n 95 new = np.zeros((n + 1, n + 1)) 96 if n > 0: 97 new[:n, :n] = self.counts 98 self.counts = new 99 100 def _recompute_gamma(self): 101 """Recompute Γ from bigram counts with Laplace smoothing.""" 102 N = len(self.configs) 103 if N == 0: 104 self.gamma = np.zeros((0, 0)) 105 return 106 # counts[src, dst]: number of times dst followed src 107 # Γ[dst, src] = p(dst | src) → column src sums to 1 108 num = self.counts.T + self.PRIOR # (N, N), rows=dst cols=src 109 self.gamma = num / num.sum(axis=0, keepdims=True) 110 111 # ── division event ──────────────────────────────────────────────────── 112 113 def press(self, key) -> dict: 114 """ 115 Process a keypress as a division event. 116 117 Returns a summary dict for the renderer. 118 """ 119 self.n_events += 1 120 new_key = (key not in self.index) 121 122 # Expand C if needed 123 if new_key: 124 self._expand(key) 125 126 # Update bigram counts: (prev → key) 127 if self.last_key is not None: 128 src = self.index[self.last_key] 129 dst = self.index[key] 130 self.counts[src, dst] += 1 131 132 # Recompute Γ from updated counts 133 prev_gamma = self.gamma 134 self._recompute_gamma() 135 136 # p(t) = Γ · eⱼ = column j of Γ (law of total probability) 137 j = self.index[key] 138 self.p = self.gamma[:, j].copy() 139 140 self.history.append(key) 141 self.prev_key = self.last_key 142 self.last_key = key 143 144 return { 145 "key": key, 146 "new_key": new_key, 147 "n_events": self.n_events, 148 "j": j, 149 "p": self.p.copy(), 150 "gamma": self.gamma.copy(), 151 "configs": list(self.configs), 152 "history": list(self.history), 153 } 154 155 def save(self, path: str): 156 """ 157 Save Gamma and the config space to a CSV file. 158 159 Format: 160 - First row: header = "key" followed by config labels (the column keys) 161 - Each subsequent row: row-key label, then Gamma[i, j] values 162 - Config order matches the matrix indices 163 """ 164 import csv 165 with open(path, 'w', newline='') as f: 166 writer = csv.writer(f) 167 writer.writerow(['key'] + [k for k in self.configs]) 168 for i, ci in enumerate(self.configs): 169 writer.writerow([ci] + ['%.6f' % self.gamma[i, j] 170 for j in range(len(self.configs))]) 171 172 def undo(self): 173 """Remove the last keypress and recompute state.""" 174 if not self.history: 175 return 176 self.history.pop() 177 self.n_events -= 1 178 179 # Rebuild from scratch 180 self.configs = [] 181 self.index = {} 182 self.counts = np.zeros((0, 0)) 183 self.gamma = None 184 self.p = None 185 self.last_key = None 186 self.prev_key = None 187 self.ck_violation = None 188 189 saved = list(self.history) 190 self.history = [] 191 for k in saved: 192 self.press(k) 193 194 195# ── Screen renderer ─────────────────────────────────────────────────────────── 196 197def fk(k): 198 """Human-readable key label.""" 199 specials = {' ': 'SPC', '\t': 'TAB', '\r': 'RET'} 200 if k in specials: 201 return specials[k] 202 if not k.isprintable(): 203 return '0x%02x' % ord(k) 204 return k 205 206W = 72 # fixed content width — never reaches terminal edge 207 208def build_screen(isp: KeyboardISP) -> str: 209 SEP = col('' * W, DIM) 210 TOP = col('' * W, CYN) 211 ln = [] 212 213 # ── header ──────────────────────────────────────────────────────────── 214 ln.append(col(" Keyboard Indivisible Stochastic Process", BLD, CYN)) 215 ln.append(TOP) 216 ln.append('') 217 218 # ── configuration space ─────────────────────────────────────────────── 219 cfgs = isp.configs 220 ln.append(col(" Configuration space C (%d keys)" % len(cfgs), BLD)) 221 if cfgs: 222 ln.append(" " + " ".join( 223 col("[%s]" % fk(k), YLW) + col("=%d" % i, DIM) 224 for i, k in enumerate(cfgs))) 225 else: 226 ln.append(col(" (none yet — press any key to begin)", DIM)) 227 ln.append('') 228 229 # ── last division event ─────────────────────────────────────────────── 230 ln.append(col(" Last division event", BLD)) 231 if isp.last_key is not None: 232 ln.append(" Observed key : " + col("[%s]" % fk(isp.last_key), CYN) 233 + col(" (p(t₀) = delta spike on this key)", DIM)) 234 if isp.prev_key is not None: 235 ln.append(" Previous key : " + col("[%s]" % fk(isp.prev_key), YLW)) 236 else: 237 ln.append(col(" (no keypresses yet)", DIM)) 238 ln.append(" Division events so far : %d" % isp.n_events) 239 ln.append('') 240 241 # ── transition matrix Γ ─────────────────────────────────────────────── 242 ln.append(col(" Transition matrix Gamma [Gamma_ij = p(i next | j just pressed)]", BLD)) 243 if isp.gamma is not None and cfgs: 244 N = len(cfgs) 245 cw = 7 246 # Column headers (source keys) 247 hdr = " " + "".join(("%*s" % (cw, fk(k))) for k in cfgs) 248 ln.append(col(hdr, YLW)) 249 for i, ci in enumerate(cfgs): 250 # Highlight the column of the last pressed key 251 row = col(" %4s " % fk(ci), YLW) 252 for j in range(N): 253 v = isp.gamma[i, j] 254 # Highlight selected column (last key pressed) 255 if isp.last_key is not None and j == isp.index[isp.last_key]: 256 clr = GRN if v > 0.3 else (YLW if v > 0.1 else DIM) 257 row += col("%*.3f" % (cw, v), clr, BLD) 258 else: 259 clr = GRN if v > 0.5 else (YLW if v > 0.2 else DIM) 260 row += col("%*.3f" % (cw, v), clr) 261 ln.append(row) 262 if isp.last_key is not None: 263 ln.append(col(" (highlighted column = Gamma·e_j for last key)", DIM)) 264 else: 265 ln.append(col(" (awaiting first keypress)", DIM)) 266 ln.append('') 267 268 # ── predicted distribution p(t) ─────────────────────────────────────── 269 ln.append(col(" p(t) = Gamma · e_j = predicted distribution over NEXT key", BLD)) 270 if isp.p is not None and cfgs: 271 bw = 30 272 # Sort by probability descending for readability 273 order = np.argsort(isp.p)[::-1] 274 for i in order: 275 v = isp.p[i] 276 f = int(v * bw) 277 bar = col('' * f, GRN) + col('' * (bw - f), DIM) 278 # Mark if this is the last observed key 279 marker = col(' <- last pressed', DIM) if cfgs[i] == isp.last_key else '' 280 ln.append(" %6s %s %.3f%s" % ( 281 col("[%s]" % fk(cfgs[i]), YLW), bar, v, marker)) 282 else: 283 ln.append(col(" (awaiting first keypress)", DIM)) 284 ln.append('') 285 286 # ── history ─────────────────────────────────────────────────────────── 287 ln.append(col(" Key history", BLD)) 288 if isp.history: 289 # Show last 40 keys 290 recent = isp.history[-40:] 291 ln.append(" " + " ".join(col("[%s]" % fk(k), CYN) for k in recent)) 292 if len(isp.history) > 40: 293 ln.append(col(" ... (%d total)" % len(isp.history), DIM)) 294 else: 295 ln.append(col(" (none yet)", DIM)) 296 ln.append('') 297 298 # ── footer ──────────────────────────────────────────────────────────── 299 ln.append(SEP) 300 if isp.last_save_path: 301 ln.append(col(" Last saved: %s" % isp.last_save_path, GRN)) 302 ln.append(col(" Every keypress is a division event | BKSP=undo | Ctrl-S=save | ESC=quit", DIM)) 303 ln.append(SEP) 304 305 return "\n".join(ln) 306 307 308class Renderer: 309 def draw(self, isp: KeyboardISP): 310 # Prefix every line with \r so cursor snaps to col 1 regardless of 311 # prior position. \033[2J clears the whole screen; \033[1;1H moves 312 # explicitly to row 1, col 1 (more reliable than \033[H in raw mode). 313 lines = build_screen(isp).split('\n') 314 out = "\033[2J\033[1;1H" + "\r\n".join(lines) 315 sys.stdout.write(out) 316 sys.stdout.flush() 317 318 319# ── Keyboard input ──────────────────────────────────────────────────────────── 320 321def read_key(fd): 322 ch = os.read(fd, 1) 323 if ch == b'\x1b': 324 fl = fcntl.fcntl(fd, fcntl.F_GETFL) 325 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) 326 try: 327 rest = os.read(fd, 8) 328 except BlockingIOError: 329 rest = b'' 330 finally: 331 fcntl.fcntl(fd, fcntl.F_SETFL, fl) 332 return '\x1b' + rest.decode('utf-8', errors='replace') 333 return ch.decode('utf-8', errors='replace') 334 335 336# ── Main ────────────────────────────────────────────────────────────────────── 337 338def main(): 339 if not sys.stdin.isatty(): 340 print("Requires an interactive terminal.", file=sys.stderr) 341 sys.exit(1) 342 343 isp = KeyboardISP() 344 renderer = Renderer() 345 fd = sys.stdin.fileno() 346 old_tty = termios.tcgetattr(fd) 347 348 # Enter alt screen, hide cursor, clear, go to 1;1 — all one atomic write 349 sys.stdout.write("\033[?1049h\033[2J\033[1;1H\033[?25l") 350 sys.stdout.flush() 351 tty.setraw(fd) 352 353 try: 354 renderer.draw(isp) 355 while True: 356 key = read_key(fd) 357 358 if key in ('\x1b', '\x03', '\x04'): # ESC / Ctrl-C / Ctrl-D 359 break 360 elif key in ('\x7f', '\x08'): # Backspace 361 isp.undo() 362 elif key == '\x13': # Ctrl-S -> save 363 if isp.gamma is not None: 364 import time 365 path = 'isp_model_%s.csv' % time.strftime('%Y%m%d_%H%M%S') 366 isp.save(path) 367 isp.last_save_path = path 368 elif key.isprintable() or key == ' ': 369 isp.press(key) 370 371 renderer.draw(isp) 372 373 except KeyboardInterrupt: 374 pass 375 finally: 376 termios.tcsetattr(fd, termios.TCSADRAIN, old_tty) 377 sys.stdout.write("\033[?25h\033[?1049l") # restore cursor then screen 378 sys.stdout.flush() 379 380 381if __name__ == "__main__": 382 main()