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 / ispGenerate.py
10 kB 282 lines
1#!/usr/bin/env python3 2""" 3ISP Generative Sampler 4====================== 5Loads a transition matrix Gamma saved by ispKeyboard.py and generates 6a stochastic sequence characteristic of the one that produced the model. 7 8Usage 9----- 10 python3 isp_generate.py <model.csv> [options] 11 12 -n, --length N number of keys to generate (default: 100) 13 -s, --start KEY starting key (default: sample from stationary dist) 14 -t, --tempo MS delay between keys in milliseconds (default: 120) 15 -o, --output FILE write generated string to file instead of stdout 16 --no-display suppress the TUI, just print the raw string 17 18Formal basis 19------------ 20At each step, the current configuration j acts as a division event: 21 p(t0) = e_j (delta spike on current key) 22 p(t) = Gamma * e_j = column j (predicted distribution over next key) 23 j' ~ p(t) (sample next key from that distribution) 24 25The generated sequence has the same bigram statistics as the original, 26and is a fresh stochastic realization of the learned indivisible law. 27""" 28 29import sys, os, time, csv, argparse, random 30import numpy as np 31 32# ── ANSI ───────────────────────────────────────────────────────────────────── 33RS = "\033[0m" 34BLD = "\033[1m" 35DIM = "\033[2m" 36CYN = "\033[96m" 37GRN = "\033[92m" 38YLW = "\033[93m" 39RED = "\033[91m" 40 41def col(text, *codes): 42 return "".join(codes) + str(text) + RS 43 44# ── Load model ──────────────────────────────────────────────────────────────── 45 46def load_model(path: str): 47 """ 48 Load Gamma and config space from a CSV saved by ispKeyboard.py. 49 50 Returns 51 ------- 52 configs : list of str — ordered key labels 53 gamma : np.ndarray — N x N column-stochastic transition matrix 54 """ 55 with open(path, newline='') as f: 56 rows = list(csv.reader(f)) 57 58 if not rows: 59 raise ValueError("Empty CSV file.") 60 61 header = rows[0] 62 if header[0] != 'key': 63 raise ValueError("Expected first column header to be 'key', got %r." % header[0]) 64 65 configs = header[1:] 66 N = len(configs) 67 68 if len(rows) - 1 != N: 69 raise ValueError("Expected %d data rows, got %d." % (N, len(rows) - 1)) 70 71 gamma = np.zeros((N, N)) 72 for i, row in enumerate(rows[1:]): 73 if row[0] != configs[i]: 74 raise ValueError( 75 "Row %d label mismatch: expected %r, got %r." % (i, configs[i], row[0])) 76 for j in range(N): 77 gamma[i, j] = float(row[j + 1]) 78 79 # Sanity check 80 col_sums = gamma.sum(axis=0) 81 if not np.allclose(col_sums, 1.0, atol=1e-4): 82 raise ValueError("Columns do not sum to 1: %s" % col_sums) 83 84 return configs, gamma 85 86 87# ── Stationary distribution ─────────────────────────────────────────────────── 88 89def stationary(gamma: np.ndarray) -> np.ndarray: 90 """ 91 Compute the stationary distribution of Gamma by finding the eigenvector 92 for eigenvalue 1 (left eigenvector of the column-stochastic matrix). 93 Falls back to the uniform distribution if computation fails. 94 """ 95 N = gamma.shape[0] 96 try: 97 vals, vecs = np.linalg.eig(gamma) 98 # Find eigenvector closest to eigenvalue 1 99 idx = np.argmin(np.abs(vals - 1.0)) 100 v = np.real(vecs[:, idx]) 101 v = np.abs(v) 102 s = v.sum() 103 if s > 1e-10: 104 return v / s 105 except Exception: 106 pass 107 return np.ones(N) / N 108 109 110# ── Sampler ─────────────────────────────────────────────────────────────────── 111 112def sample_sequence(configs, gamma, length: int, start_idx: int) -> "list[int]": 113 """ 114 Generate a sequence of config indices by repeatedly: 115 p(t) = column j of Gamma 116 j' ~ p(t) 117 """ 118 N = len(configs) 119 seq = [start_idx] 120 j = start_idx 121 rng = np.random.default_rng() 122 for _ in range(length - 1): 123 p = gamma[:, j] 124 p = p / p.sum() # renormalize to guard against floating-point drift 125 j = int(rng.choice(N, p=p)) 126 seq.append(j) 127 return seq 128 129 130# ── Display ─────────────────────────────────────────────────────────────────── 131 132def fk(k: str) -> str: 133 return {' ': '', '\t': '', '\r': ''}.get(k, k) 134 135W = 72 136 137def run_display(configs, gamma, seq: "list[int]", tempo_ms: int): 138 """ 139 Animate the generation in a TUI: show the active column of Gamma 140 and the growing output string, one key at a time. 141 """ 142 import tty, termios, fcntl 143 144 N = len(configs) 145 delay = tempo_ms / 1000.0 146 output = [] 147 148 def build(current_idx: int, next_idx: int) -> str: 149 ln = [] 150 151 ln.append(col(" ISP Generative Sampler", BLD, CYN)) 152 ln.append(col("" * W, CYN)) 153 ln.append('') 154 155 # Current division event 156 ln.append(col(" Current division event", BLD)) 157 ln.append(" Key j : " + col("[%s]" % fk(configs[current_idx]), CYN) 158 + col(" p(t0) = delta spike", DIM)) 159 ln.append('') 160 161 # Active column of Gamma = p(t) 162 ln.append(col(" p(t) = Gamma * e_j = column j [predicted next key]", BLD)) 163 p = gamma[:, current_idx] 164 bw = 30 165 order = np.argsort(p)[::-1] 166 for i in order: 167 v = p[i] 168 f = int(v * bw) 169 bar = col('' * f, GRN) + col('' * (bw - f), DIM) 170 picked = col(' <- sampled', YLW) if i == next_idx else '' 171 ln.append(" %6s %s %.3f%s" % ( 172 col("[%s]" % fk(configs[i]), YLW), bar, v, picked)) 173 ln.append('') 174 175 # Generated string so far 176 ln.append(col(" Generated output", BLD)) 177 raw = ''.join(configs[i] for i in output) 178 # Wrap at W-4 chars 179 chunk = W - 4 180 lines = [raw[i:i+chunk] for i in range(0, max(len(raw), 1), chunk)] 181 for line in lines[-4:]: # show last 4 rows 182 ln.append(" " + col(line, CYN)) 183 ln.append(col(" (%d keys generated)" % len(output), DIM)) 184 ln.append('') 185 186 ln.append(col("" * W, DIM)) 187 ln.append(col(" Ctrl-C to stop early", DIM)) 188 ln.append(col("" * W, DIM)) 189 190 return "\r\n".join(ln) 191 192 # Enter alternate screen 193 sys.stdout.write("\033[?1049h\033[2J\033[1;1H\033[?25l") 194 sys.stdout.flush() 195 196 try: 197 for step in range(len(seq) - 1): 198 current_idx = seq[step] 199 next_idx = seq[step + 1] 200 output.append(current_idx) 201 202 sys.stdout.write("\033[2J\033[1;1H" + build(current_idx, next_idx)) 203 sys.stdout.flush() 204 time.sleep(delay) 205 206 # Final key 207 output.append(seq[-1]) 208 sys.stdout.write("\033[2J\033[1;1H" + build(seq[-1], seq[-1])) 209 sys.stdout.flush() 210 time.sleep(1.0) 211 212 except KeyboardInterrupt: 213 pass 214 finally: 215 sys.stdout.write("\033[?25h\033[?1049l") 216 sys.stdout.flush() 217 218 return ''.join(configs[i] for i in output) 219 220 221# ── Main ────────────────────────────────────────────────────────────────────── 222 223def main(): 224 parser = argparse.ArgumentParser( 225 description="Generate a stochastic sequence from an ISP model CSV.") 226 parser.add_argument('model', 227 help="Path to the CSV model file saved by ispKeyboard.py") 228 parser.add_argument('-n', '--length', type=int, default=100, 229 metavar='N', help="Number of keys to generate (default: 100)") 230 parser.add_argument('-s', '--start', type=str, default=None, 231 metavar='KEY', help="Starting key (default: sample from stationary dist)") 232 parser.add_argument('-t', '--tempo', type=int, default=120, 233 metavar='MS', help="Delay between keys in milliseconds (default: 120)") 234 parser.add_argument('-o', '--output', type=str, default=None, 235 metavar='FILE', help="Write generated string to file") 236 parser.add_argument('--no-display', action='store_true', 237 help="Skip the TUI, print raw string to stdout") 238 args = parser.parse_args() 239 240 # Load 241 try: 242 configs, gamma = load_model(args.model) 243 except (FileNotFoundError, ValueError) as e: 244 print("Error loading model: %s" % e, file=sys.stderr) 245 sys.exit(1) 246 247 N = len(configs) 248 print("Loaded model: %d keys, %d x %d matrix" % (N, N, N), file=sys.stderr) 249 250 # Determine start key 251 if args.start is not None: 252 if args.start not in configs: 253 print("Error: start key %r not in model config space %s" 254 % (args.start, configs), file=sys.stderr) 255 sys.exit(1) 256 start_idx = configs.index(args.start) 257 else: 258 stat = stationary(gamma) 259 start_idx = int(np.random.default_rng().choice(N, p=stat)) 260 print("Sampled start key: %r (from stationary dist)" % configs[start_idx], 261 file=sys.stderr) 262 263 # Generate sequence 264 seq = sample_sequence(configs, gamma, args.length, start_idx) 265 266 # Output 267 if args.no_display or not sys.stdout.isatty(): 268 result = ''.join(configs[i] for i in seq) 269 else: 270 result = run_display(configs, gamma, seq, args.tempo) 271 272 if args.output: 273 with open(args.output, 'w') as f: 274 f.write(result) 275 print("Written to %s" % args.output, file=sys.stderr) 276 else: 277 # Print to stdout (after TUI exits) 278 print(result) 279 280 281if __name__ == "__main__": 282 main()