TuringsSolutions commited on
Commit
2ff4d2d
·
verified ·
1 Parent(s): 1d677f6

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +831 -0
app.py ADDED
@@ -0,0 +1,831 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # FLC v1.3 — Standalone Fibonacci Lattice Compression
3
+ # + Real .flc file format (v2 header)
4
+ # + Hologram preview PNG
5
+ # + "Holographic Unzip" progressive GIF (REAL partial reconstructions)
6
+ # + max_bands horizon control
7
+ # + Fibonacci spiral unfold layout (right panel)
8
+ # + NEW: Event-horizon ring overlay (left panel)
9
+ # + NEW: Fibonacci tile outlines (right panel)
10
+ #
11
+ # Notebook usage:
12
+ # enc = flc_encode_file("in.bin","out.flc","preview.png", unzip_gif="unzip.gif",
13
+ # block_len=1024, n_bands=10, base_step=0.004)
14
+ # dec = flc_decode_file("out.flc","recovered.bin", unzip_gif="decode_unzip.gif", max_bands=None)
15
+ #
16
+ # Partial/horizon:
17
+ # decp = flc_decode_file("out.flc","partial.bin", unzip_gif="partial_unzip.gif", max_bands=4)
18
+ #
19
+ # Depends: numpy, pillow
20
+ # ============================================================
21
+
22
+ import os, struct, math, argparse
23
+ import numpy as np
24
+ from PIL import Image, ImageDraw
25
+
26
+ PHI = (1.0 + 5.0**0.5) / 2.0
27
+ MAGIC = b"FLC1\x00\x00\x00\x00" # 8 bytes
28
+ VER_V2 = 2 # float64 params
29
+
30
+ # --------------------------
31
+ # Fibonacci utilities
32
+ # --------------------------
33
+ def fibonacci_sequence(n):
34
+ fibs = [1, 2]
35
+ while len(fibs) < n:
36
+ fibs.append(fibs[-1] + fibs[-2])
37
+ return np.array(fibs[:n], dtype=np.int64)
38
+
39
+ def fibonacci_sequence_std(n):
40
+ fibs = [1, 1]
41
+ while len(fibs) < n:
42
+ fibs.append(fibs[-1] + fibs[-2])
43
+ return np.array(fibs[:n], dtype=np.int64)
44
+
45
+ def fibonacci_frequency_boundaries(n_coeffs: int, n_bands: int):
46
+ """
47
+ Strict: returns exactly n_bands+1 boundaries => exactly n_bands intervals.
48
+ Deterministic repair enforces strict monotone boundaries.
49
+ """
50
+ if n_bands < 2:
51
+ return [0, n_coeffs]
52
+
53
+ fibs = fibonacci_sequence(n_bands).astype(np.float64)
54
+ w = fibs / (fibs.sum() + 1e-12)
55
+ cum = np.cumsum(w)
56
+
57
+ b = [0]
58
+ for i in range(n_bands - 1):
59
+ bi = int(round(n_coeffs * cum[i]))
60
+ b.append(bi)
61
+ b.append(n_coeffs)
62
+
63
+ b = [max(0, min(n_coeffs, int(x))) for x in b]
64
+ b[0] = 0
65
+ b[-1] = n_coeffs
66
+
67
+ # strictify forward
68
+ for i in range(1, len(b) - 1):
69
+ if b[i] <= b[i-1]:
70
+ b[i] = b[i-1] + 1
71
+ # strictify backward
72
+ for i in range(len(b) - 2, 0, -1):
73
+ if b[i] >= b[i+1]:
74
+ b[i] = b[i+1] - 1
75
+
76
+ # remove invalid interior points
77
+ b = [0] + [x for x in b[1:-1] if 0 < x < n_coeffs] + [n_coeffs]
78
+
79
+ # repack if we lost points (n_coeffs small or rounding)
80
+ while len(b) < n_bands + 1:
81
+ gaps = [(b[i+1] - b[i], i) for i in range(len(b) - 1)]
82
+ gaps.sort(reverse=True)
83
+ _, gi = gaps[0]
84
+ mid = (b[gi] + b[gi+1]) // 2
85
+ if mid == b[gi] or mid == b[gi+1]:
86
+ break
87
+ b.insert(gi+1, mid)
88
+
89
+ # trim if we have too many
90
+ while len(b) > n_bands + 1:
91
+ gaps = [(b[i+1] - b[i], i) for i in range(len(b) - 1)]
92
+ gaps.sort() # smallest first
93
+ _, gi = gaps[0]
94
+ if 0 < gi+1 < len(b)-1:
95
+ b.pop(gi+1)
96
+ else:
97
+ break
98
+
99
+ return b
100
+
101
+ # --------------------------
102
+ # Orthonormal DCT-II / IDCT (no scipy)
103
+ # --------------------------
104
+ def dct_ortho_1d(x: np.ndarray) -> np.ndarray:
105
+ x = np.asarray(x, dtype=np.float64)
106
+ N = x.shape[0]
107
+ v = np.concatenate([x, x[::-1]])
108
+ V = np.fft.fft(v)
109
+ k = np.arange(N)
110
+ X = np.real(V[:N] * np.exp(-1j * np.pi * k / (2 * N)))
111
+ X *= 2.0
112
+ X[0] *= (1.0 / math.sqrt(4 * N))
113
+ X[1:] *= (1.0 / math.sqrt(2 * N))
114
+ return X
115
+
116
+ def idct_ortho_1d(X: np.ndarray) -> np.ndarray:
117
+ X = np.asarray(X, dtype=np.float64)
118
+ N = X.shape[0]
119
+ x = X.copy()
120
+
121
+ x0 = x[0] * math.sqrt(4 * N)
122
+ xr = x[1:] * math.sqrt(2 * N)
123
+ c = np.empty(N, dtype=np.complex128)
124
+ c[0] = x0 / 2.0
125
+ c[1:] = xr / 2.0
126
+
127
+ k = np.arange(N)
128
+ c = c * np.exp(1j * np.pi * k / (2 * N))
129
+
130
+ V = np.zeros(2 * N, dtype=np.complex128)
131
+ V[:N] = c
132
+ V[N+1:] = np.conj(c[1:][::-1])
133
+ v = np.fft.ifft(V).real
134
+ return v[:N]
135
+
136
+ def dct_blocks_ortho(x_blocks: np.ndarray) -> np.ndarray:
137
+ B, L = x_blocks.shape
138
+ out = np.empty((B, L), dtype=np.float64)
139
+ for i in range(B):
140
+ out[i] = dct_ortho_1d(x_blocks[i])
141
+ return out
142
+
143
+ def idct_blocks_ortho(X_blocks: np.ndarray) -> np.ndarray:
144
+ B, L = X_blocks.shape
145
+ out = np.empty((B, L), dtype=np.float64)
146
+ for i in range(B):
147
+ out[i] = idct_ortho_1d(X_blocks[i])
148
+ return out
149
+
150
+ # --------------------------
151
+ # Bit IO
152
+ # --------------------------
153
+ class BitWriter:
154
+ def __init__(self):
155
+ self.buf = bytearray()
156
+ self.acc = 0
157
+ self.nbits = 0
158
+
159
+ def write_bit(self, b: int):
160
+ self.acc = (self.acc << 1) | (b & 1)
161
+ self.nbits += 1
162
+ if self.nbits == 8:
163
+ self.buf.append(self.acc & 0xFF)
164
+ self.acc = 0
165
+ self.nbits = 0
166
+
167
+ def finish(self) -> bytes:
168
+ if self.nbits:
169
+ self.acc <<= (8 - self.nbits)
170
+ self.buf.append(self.acc & 0xFF)
171
+ self.acc = 0
172
+ self.nbits = 0
173
+ return bytes(self.buf)
174
+
175
+ class BitReader:
176
+ def __init__(self, data: bytes):
177
+ self.data = data
178
+ self.i = 0
179
+ self.acc = 0
180
+ self.nbits = 0
181
+
182
+ def read_bit(self) -> int:
183
+ if self.nbits == 0:
184
+ if self.i >= len(self.data):
185
+ raise EOFError("BitReader EOF")
186
+ self.acc = self.data[self.i]
187
+ self.i += 1
188
+ self.nbits = 8
189
+ b = (self.acc >> (self.nbits - 1)) & 1
190
+ self.nbits -= 1
191
+ return b
192
+
193
+ # --------------------------
194
+ # Fibonacci coding
195
+ # --------------------------
196
+ def _fib_numbers_upto(n: int):
197
+ fibs = [1, 2]
198
+ while fibs[-1] <= n:
199
+ fibs.append(fibs[-1] + fibs[-2])
200
+ return fibs
201
+
202
+ def fib_encode_nonneg(bw: BitWriter, n: int):
203
+ m = int(n) + 1
204
+ fibs = _fib_numbers_upto(m)
205
+ bits = [0] * (len(fibs) - 1)
206
+ rem = m
207
+ for i in reversed(range(len(bits))):
208
+ f = fibs[i]
209
+ if f <= rem:
210
+ bits[i] = 1
211
+ rem -= f
212
+ hi = max((i for i, b in enumerate(bits) if b), default=0)
213
+ for i in range(hi + 1):
214
+ bw.write_bit(bits[i])
215
+ bw.write_bit(1)
216
+
217
+ def fib_decode_nonneg(br: BitReader) -> int:
218
+ fibs = [1, 2]
219
+ bits = []
220
+ prev = 0
221
+ while True:
222
+ b = br.read_bit()
223
+ bits.append(b)
224
+ if prev == 1 and b == 1:
225
+ break
226
+ prev = b
227
+ if len(bits) > len(fibs):
228
+ fibs.append(fibs[-1] + fibs[-2])
229
+ payload = bits[:-1]
230
+ while len(fibs) < len(payload):
231
+ fibs.append(fibs[-1] + fibs[-2])
232
+ m = 0
233
+ for i, bi in enumerate(payload):
234
+ if bi:
235
+ m += fibs[i]
236
+ return m - 1
237
+
238
+ def zigzag_encode_i64(x: int) -> int:
239
+ x = int(x)
240
+ return (x << 1) ^ (x >> 63)
241
+
242
+ def zigzag_decode_i64(u: int) -> int:
243
+ u = int(u)
244
+ return (u >> 1) ^ (-(u & 1))
245
+
246
+ def rle_fib_encode_ints(ints: np.ndarray) -> bytes:
247
+ bw = BitWriter()
248
+ ints = ints.astype(np.int64, copy=False)
249
+ zrun = 0
250
+ for v in ints:
251
+ if v == 0:
252
+ zrun += 1
253
+ continue
254
+ if zrun:
255
+ bw.write_bit(0)
256
+ fib_encode_nonneg(bw, zrun)
257
+ zrun = 0
258
+ bw.write_bit(1)
259
+ fib_encode_nonneg(bw, zigzag_encode_i64(int(v)))
260
+ if zrun:
261
+ bw.write_bit(0)
262
+ fib_encode_nonneg(bw, zrun)
263
+ return bw.finish()
264
+
265
+ def rle_fib_decode_ints(payload: bytes, n_out: int) -> np.ndarray:
266
+ br = BitReader(payload)
267
+ out = np.zeros(n_out, dtype=np.int64)
268
+ i = 0
269
+ while i < n_out:
270
+ tag = br.read_bit()
271
+ if tag == 0:
272
+ k = fib_decode_nonneg(br)
273
+ i = min(n_out, i + k)
274
+ else:
275
+ u = fib_decode_nonneg(br)
276
+ out[i] = zigzag_decode_i64(u)
277
+ i += 1
278
+ return out
279
+
280
+ def delta1d(x: np.ndarray) -> np.ndarray:
281
+ x = x.astype(np.int64, copy=False)
282
+ out = np.empty_like(x, dtype=np.int64)
283
+ prev = 0
284
+ for i, v in enumerate(x):
285
+ out[i] = int(v) - prev
286
+ prev = int(v)
287
+ return out
288
+
289
+ def inv_delta1d(d: np.ndarray) -> np.ndarray:
290
+ d = d.astype(np.int64, copy=False)
291
+ out = np.empty_like(d, dtype=np.int64)
292
+ acc = 0
293
+ for i, v in enumerate(d):
294
+ acc += int(v)
295
+ out[i] = acc
296
+ return out
297
+
298
+ # --------------------------
299
+ # φ-scaled band quantization
300
+ # --------------------------
301
+ def band_quantize_dct(coeffs: np.ndarray, boundaries, base_step: float, phi: float = PHI):
302
+ B, L = coeffs.shape
303
+ q = np.zeros((B, L), dtype=np.int32)
304
+ for bi in range(len(boundaries) - 1):
305
+ a, b = boundaries[bi], boundaries[bi + 1]
306
+ if a >= b:
307
+ continue
308
+ step = base_step * (phi ** bi)
309
+ q[:, a:b] = np.round(coeffs[:, a:b] / step).astype(np.int32)
310
+ return q
311
+
312
+ def band_dequantize_dct(q: np.ndarray, boundaries, base_step: float, phi: float = PHI):
313
+ B, L = q.shape
314
+ coeffs = np.zeros((B, L), dtype=np.float64)
315
+ for bi in range(len(boundaries) - 1):
316
+ a, b = boundaries[bi], boundaries[bi + 1]
317
+ if a >= b:
318
+ continue
319
+ step = base_step * (phi ** bi)
320
+ coeffs[:, a:b] = q[:, a:b].astype(np.float64) * step
321
+ return coeffs
322
+
323
+ # --------------------------
324
+ # Hologram (left panel) + horizon ring overlay
325
+ # --------------------------
326
+ def choose_fib_grid(n_symbols: int) -> int:
327
+ N = int(math.ceil(math.sqrt(n_symbols)))
328
+ fibs = [1, 2]
329
+ while fibs[-1] < N:
330
+ fibs.append(fibs[-1] + fibs[-2])
331
+ return fibs[-1]
332
+
333
+ def ints_to_constellation(zints: np.ndarray):
334
+ zints = zints.astype(np.int64)
335
+ v = np.tanh(zints / 32.0).astype(np.float64)
336
+ k = np.arange(v.size, dtype=np.float64)
337
+ golden = 2 * math.pi / (PHI**2)
338
+ theta = golden * k + 2.0 * math.pi * (v * 0.25)
339
+ r = 1.0 + 0.35 * np.abs(v)
340
+ return (r * np.cos(theta) + 1j * r * np.sin(theta)).astype(np.complex64)
341
+
342
+ def hologram_spectrum_image(zints: np.ndarray, max_symbols: int = 262144):
343
+ if zints.size > max_symbols:
344
+ zints = zints[:max_symbols]
345
+ syms = ints_to_constellation(zints)
346
+ N = choose_fib_grid(syms.size)
347
+ total = N * N
348
+ if syms.size < total:
349
+ syms = np.pad(syms, (0, total - syms.size), constant_values=(1+0j)).astype(np.complex64)
350
+ U = syms.reshape(N, N)
351
+ F = np.fft.fftshift(np.fft.fft2(U))
352
+ mag = np.log1p(np.abs(F)).astype(np.float32)
353
+ mag -= mag.min()
354
+ mag /= (mag.max() + 1e-12)
355
+ return (mag * 255).astype(np.uint8)
356
+
357
+ def make_hologram_preview(zints: np.ndarray, out_png: str, max_symbols: int = 262144):
358
+ img = hologram_spectrum_image(zints, max_symbols=max_symbols)
359
+ Image.fromarray(img).save(out_png)
360
+
361
+ def overlay_event_horizon_ring(imL: Image.Image, t: int, T: int):
362
+ """
363
+ Draw a glowing event-horizon ring (as 3 concentric ellipses) on the hologram.
364
+ Radius expands with progress t/T.
365
+ """
366
+ W, H = imL.size
367
+ cx, cy = W // 2, H // 2
368
+ # start small, expand
369
+ r0 = int(0.08 * min(W, H))
370
+ r1 = int((0.45 * min(W, H)) * (t / max(1, T)) + r0)
371
+
372
+ rgb = imL.convert("RGB")
373
+ d = ImageDraw.Draw(rgb)
374
+
375
+ # glow: 3 rings
376
+ for k, alpha in [(0, 255), (3, 170), (6, 90)]:
377
+ rr = r1 + k
378
+ bbox = (cx - rr, cy - rr, cx + rr, cy + rr)
379
+ # gold-ish ring
380
+ col = (255, 215, 0) if alpha == 255 else (220, 180, 0)
381
+ d.ellipse(bbox, outline=col, width=2)
382
+
383
+ # center dot (singularity hint)
384
+ d.ellipse((cx-2, cy-2, cx+2, cy+2), fill=(255, 215, 0))
385
+ return rgb
386
+
387
+ # --------------------------
388
+ # Fibonacci spiral unfold (right panel) + tile outlines
389
+ # --------------------------
390
+ def fibonacci_spiral_tiles_for_area(n_pixels: int, max_tiles: int = 30):
391
+ fibs = fibonacci_sequence_std(max_tiles) # 1,1,2,3,5...
392
+ sizes = []
393
+ area = 0
394
+ for s in fibs:
395
+ sizes.append(int(s))
396
+ area += int(s) * int(s)
397
+ if area >= n_pixels:
398
+ break
399
+ return sizes
400
+
401
+ def fibonacci_spiral_tile_positions(sizes):
402
+ tiles = []
403
+ s0 = sizes[0]
404
+ minx, miny, maxx, maxy = 0, 0, s0, s0
405
+ tiles.append((0, 0, s0))
406
+
407
+ for i in range(1, len(sizes)):
408
+ s = sizes[i]
409
+ d = (i - 1) % 4 # right, up, left, down
410
+ if d == 0: # right
411
+ x, y = maxx, miny
412
+ elif d == 1: # up
413
+ x, y = maxx - s, maxy
414
+ elif d == 2: # left
415
+ x, y = minx - s, maxy - s
416
+ else: # down
417
+ x, y = minx, miny - s
418
+
419
+ tiles.append((x, y, s))
420
+ minx = min(minx, x)
421
+ miny = min(miny, y)
422
+ maxx = max(maxx, x + s)
423
+ maxy = max(maxy, y + s)
424
+
425
+ bbox = (minx, miny, maxx, maxy)
426
+ return tiles, bbox
427
+
428
+ def bytes_to_fib_spiral_image(data: bytes, max_pixels: int = 262144, want_tiles=False):
429
+ arr = np.frombuffer(data, dtype=np.uint8)
430
+ if arr.size > max_pixels:
431
+ arr = arr[:max_pixels]
432
+ n = int(arr.size)
433
+ if n == 0:
434
+ if want_tiles:
435
+ return np.zeros((1, 1), dtype=np.uint8), [(0,0,1)], (0,0,1,1)
436
+ return np.zeros((1, 1), dtype=np.uint8)
437
+
438
+ sizes = fibonacci_spiral_tiles_for_area(n_pixels=n, max_tiles=32)
439
+ tiles, (minx, miny, maxx, maxy) = fibonacci_spiral_tile_positions(sizes)
440
+
441
+ W = int(maxx - minx)
442
+ H = int(maxy - miny)
443
+ img = np.zeros((H, W), dtype=np.uint8)
444
+ idx = 0
445
+
446
+ for (x, y, s) in tiles:
447
+ if idx >= n:
448
+ break
449
+ x0 = int(x - minx)
450
+ y0_up = int(y - miny)
451
+ y0 = int((H - (y0_up + s)))
452
+
453
+ take = min(s * s, n - idx)
454
+ block = arr[idx:idx + take]
455
+ if take < s * s:
456
+ block = np.pad(block, (0, s * s - take), constant_values=0)
457
+ block2d = block.reshape(s, s)
458
+ img[y0:y0+s, x0:x0+s] = block2d
459
+ idx += take
460
+
461
+ if want_tiles:
462
+ return img, tiles, (minx, miny, maxx, maxy)
463
+ return img
464
+
465
+ def overlay_tile_outlines(imL: Image.Image, tiles, bbox_info, outline=(120,120,120)):
466
+ """
467
+ Draw square outlines for the Fibonacci tiling on the right panel.
468
+ `tiles` are in y-up coords with original bbox min offsets.
469
+ """
470
+ rgb = imL.convert("RGB")
471
+ d = ImageDraw.Draw(rgb)
472
+ # rebuild H,W from image
473
+ W, H = rgb.size
474
+
475
+ # bbox is y-up coords: minx,miny,maxx,maxy
476
+ minx, miny, maxx, maxy = bbox_info
477
+ # For each tile, map y-up to y-down in image coords
478
+ for (x, y, s) in tiles:
479
+ x0 = int(x - minx)
480
+ y0_up = int(y - miny)
481
+ y0 = int((H - (y0_up + s)))
482
+ d.rectangle((x0, y0, x0 + s - 1, y0 + s - 1), outline=outline, width=1)
483
+ return rgb
484
+
485
+ # --------------------------
486
+ # GIF assembly
487
+ # --------------------------
488
+ def save_gif(frames, out_gif: str, duration=90):
489
+ if not frames:
490
+ return
491
+ frames[0].save(out_gif, save_all=True, append_images=frames[1:], duration=duration, loop=0, optimize=True)
492
+
493
+ def make_unzip_gif_from_Q(Q_full: np.ndarray,
494
+ boundaries,
495
+ base_step: float,
496
+ mu: float,
497
+ n_bytes: int,
498
+ out_gif: str,
499
+ max_bands: int = None,
500
+ gif_scale: int = 2,
501
+ max_symbols: int = 262144,
502
+ max_pixels: int = 262144,
503
+ draw_outlines: bool = True,
504
+ draw_horizon: bool = True):
505
+ n_total_bands = len(boundaries) - 1
506
+ T = n_total_bands if max_bands is None else max(0, min(int(max_bands), n_total_bands))
507
+ if T == 0:
508
+ T = 1
509
+
510
+ frames = []
511
+ band_slices = [(boundaries[i], boundaries[i+1]) for i in range(n_total_bands)]
512
+
513
+ for t in range(1, T + 1):
514
+ Q_part = np.zeros_like(Q_full)
515
+ for bi in range(t):
516
+ a, b = band_slices[bi]
517
+ if a < b:
518
+ Q_part[:, a:b] = Q_full[:, a:b]
519
+
520
+ # partial recon bytes
521
+ C_part = band_dequantize_dct(Q_part, boundaries, base_step=base_step, phi=PHI)
522
+ X_part = idct_blocks_ortho(C_part)
523
+ x = X_part.reshape(-1)[:n_bytes]
524
+ x = (x * 127.5) + float(mu)
525
+ x = np.clip(np.round(x), 0, 255).astype(np.uint8).tobytes()
526
+
527
+ # left hologram
528
+ qflat = Q_part.reshape(-1).astype(np.int64)
529
+ holo_u8 = hologram_spectrum_image(qflat, max_symbols=max_symbols)
530
+ A = Image.fromarray(holo_u8).convert("L")
531
+ if gif_scale != 1:
532
+ A = A.resize((A.size[0]*gif_scale, A.size[1]*gif_scale), Image.NEAREST)
533
+
534
+ # horizon ring overlay
535
+ if draw_horizon:
536
+ A_rgb = overlay_event_horizon_ring(A, t=t, T=T)
537
+ else:
538
+ A_rgb = A.convert("RGB")
539
+
540
+ # right spiral image
541
+ field_u8, tiles, bbox_info = bytes_to_fib_spiral_image(x, max_pixels=max_pixels, want_tiles=True)
542
+ Bimg = Image.fromarray(field_u8).convert("L")
543
+ if gif_scale != 1:
544
+ Bimg = Bimg.resize((Bimg.size[0]*gif_scale, Bimg.size[1]*gif_scale), Image.NEAREST)
545
+
546
+ if draw_outlines:
547
+ # scale tile coordinates too (by rendering outlines after scaling)
548
+ # easiest: draw outlines on scaled image using scaled bbox & tiles
549
+ # so adjust bbox and tiles by gif_scale
550
+ minx, miny, maxx, maxy = bbox_info
551
+ tiles_s = [(x*gif_scale, y*gif_scale, s*gif_scale) for (x,y,s) in tiles]
552
+ bbox_s = (minx*gif_scale, miny*gif_scale, maxx*gif_scale, maxy*gif_scale)
553
+ B_rgb = overlay_tile_outlines(Bimg, tiles_s, bbox_s, outline=(120,120,120))
554
+ else:
555
+ B_rgb = Bimg.convert("RGB")
556
+
557
+ # match heights by padding
558
+ H = max(A_rgb.size[1], B_rgb.size[1])
559
+ def pad_to_h_rgb(im, H):
560
+ if im.size[1] == H:
561
+ return im
562
+ canvas = Image.new("RGB", (im.size[0], H), (0,0,0))
563
+ canvas.paste(im, (0, (H - im.size[1])//2))
564
+ return canvas
565
+ A_rgb = pad_to_h_rgb(A_rgb, H)
566
+ B_rgb = pad_to_h_rgb(B_rgb, H)
567
+
568
+ W = A_rgb.size[0]
569
+ canvas = Image.new("RGB", (W*2, H + 44), (0, 0, 0))
570
+ canvas.paste(A_rgb, (0, 44))
571
+ canvas.paste(B_rgb, (W, 44))
572
+
573
+ draw = ImageDraw.Draw(canvas)
574
+ draw.text((12, 10), f"FLC HOLOGRAPHIC UNZIP | bands {t}/{T} (horizon={T})", fill=(255, 215, 0))
575
+ draw.text((12, 26), "LEFT: hologram + event horizon | RIGHT: Fibonacci spiral + tile outlines", fill=(180, 180, 180))
576
+ frames.append(canvas)
577
+
578
+ save_gif(frames, out_gif, duration=90)
579
+
580
+ # ============================================================
581
+ # Encoder / Decoder
582
+ # ============================================================
583
+ def flc_encode_file(in_path: str,
584
+ out_flc: str,
585
+ preview_png: str = None,
586
+ unzip_gif: str = None,
587
+ block_len: int = 1024,
588
+ n_bands: int = 10,
589
+ base_step: float = 0.004,
590
+ use_mean_center: bool = True,
591
+ gif_scale: int = 2,
592
+ gif_horizon: int = None,
593
+ draw_horizon: bool = True,
594
+ draw_outlines: bool = True):
595
+ raw = open(in_path, "rb").read()
596
+ n_bytes = len(raw)
597
+
598
+ x = np.frombuffer(raw, dtype=np.uint8).astype(np.float64)
599
+ mu = float(np.mean(x)) if use_mean_center else 127.5
600
+ x = (x - mu) / 127.5
601
+
602
+ pad = (-x.size) % block_len
603
+ if pad:
604
+ x = np.pad(x, (0, pad), constant_values=0.0)
605
+ n_blocks = x.size // block_len
606
+ X = x.reshape(n_blocks, block_len)
607
+
608
+ C = dct_blocks_ortho(X)
609
+ boundaries = fibonacci_frequency_boundaries(block_len, n_bands)
610
+ Q = band_quantize_dct(C, boundaries, base_step=base_step, phi=PHI)
611
+
612
+ q_flat = Q.reshape(-1).astype(np.int64)
613
+ d = delta1d(q_flat)
614
+ payload = rle_fib_encode_ints(d)
615
+
616
+ header = struct.pack(
617
+ "<8sH Q I I H d d H",
618
+ MAGIC, VER_V2,
619
+ int(n_bytes),
620
+ int(block_len),
621
+ int(n_blocks),
622
+ int(n_bands),
623
+ float(base_step),
624
+ float(mu),
625
+ int(len(boundaries)),
626
+ )
627
+ bnds = struct.pack("<" + "I"*len(boundaries), *[int(b) for b in boundaries])
628
+ paylen = struct.pack("<I", int(len(payload)))
629
+
630
+ with open(out_flc, "wb") as f:
631
+ f.write(header)
632
+ f.write(bnds)
633
+ f.write(paylen)
634
+ f.write(payload)
635
+
636
+ if preview_png is not None:
637
+ make_hologram_preview(q_flat, preview_png)
638
+
639
+ if unzip_gif is not None:
640
+ make_unzip_gif_from_Q(
641
+ Q_full=Q,
642
+ boundaries=boundaries,
643
+ base_step=float(base_step),
644
+ mu=float(mu),
645
+ n_bytes=int(n_bytes),
646
+ out_gif=unzip_gif,
647
+ max_bands=gif_horizon,
648
+ gif_scale=gif_scale,
649
+ draw_horizon=draw_horizon,
650
+ draw_outlines=draw_outlines,
651
+ )
652
+
653
+ return {
654
+ "n_bytes": int(n_bytes),
655
+ "block_len": int(block_len),
656
+ "n_blocks": int(n_blocks),
657
+ "n_bands": int(n_bands),
658
+ "base_step": float(base_step),
659
+ "mu": float(mu),
660
+ "payload_len": int(len(payload)),
661
+ "out_flc": out_flc,
662
+ "preview_png": preview_png,
663
+ "unzip_gif": unzip_gif,
664
+ "ratio": (os.path.getsize(out_flc) / max(1, n_bytes)),
665
+ }
666
+
667
+ def flc_decode_file(in_flc: str,
668
+ out_path: str,
669
+ unzip_gif: str = None,
670
+ max_bands: int = None,
671
+ gif_scale: int = 2,
672
+ draw_horizon: bool = True,
673
+ draw_outlines: bool = True):
674
+ blob = open(in_flc, "rb").read()
675
+ off = 0
676
+
677
+ (magic, ver, n_bytes, block_len, n_blocks, n_bands, base_step, mu, bnd_len) = struct.unpack_from(
678
+ "<8sH Q I I H d d H", blob, off
679
+ )
680
+ off += struct.calcsize("<8sH Q I I H d d H")
681
+ if magic != MAGIC:
682
+ raise ValueError("Bad magic (not FLC1)")
683
+ if ver != VER_V2:
684
+ raise ValueError(f"Unsupported version {ver}")
685
+
686
+ boundaries = list(struct.unpack_from("<" + "I"*bnd_len, blob, off))
687
+ off += 4 * bnd_len
688
+
689
+ (payload_len,) = struct.unpack_from("<I", blob, off)
690
+ off += 4
691
+ payload = blob[off:off+payload_len]
692
+ off += payload_len
693
+
694
+ n_coeffs = int(n_blocks) * int(block_len)
695
+ d = rle_fib_decode_ints(payload, n_coeffs)
696
+ q_flat = inv_delta1d(d).astype(np.int64)
697
+ Q_full = q_flat.reshape(int(n_blocks), int(block_len)).astype(np.int32)
698
+
699
+ # horizon
700
+ n_total_bands = len(boundaries) - 1
701
+ bands_to_apply = n_total_bands if max_bands is None else max(0, min(int(max_bands), n_total_bands))
702
+
703
+ if bands_to_apply < n_total_bands:
704
+ Q_use = np.zeros_like(Q_full)
705
+ for bi in range(bands_to_apply):
706
+ a, b = boundaries[bi], boundaries[bi+1]
707
+ if a < b:
708
+ Q_use[:, a:b] = Q_full[:, a:b]
709
+ else:
710
+ Q_use = Q_full
711
+
712
+ C = band_dequantize_dct(Q_use, boundaries, base_step=float(base_step), phi=PHI)
713
+ X = idct_blocks_ortho(C)
714
+
715
+ x = X.reshape(-1)[:int(n_bytes)]
716
+ x = (x * 127.5) + float(mu)
717
+ x = np.clip(np.round(x), 0, 255).astype(np.uint8)
718
+
719
+ with open(out_path, "wb") as f:
720
+ f.write(x.tobytes())
721
+
722
+ if unzip_gif is not None:
723
+ make_unzip_gif_from_Q(
724
+ Q_full=Q_full,
725
+ boundaries=boundaries,
726
+ base_step=float(base_step),
727
+ mu=float(mu),
728
+ n_bytes=int(n_bytes),
729
+ out_gif=unzip_gif,
730
+ max_bands=(bands_to_apply if bands_to_apply > 0 else 1),
731
+ gif_scale=gif_scale,
732
+ draw_horizon=draw_horizon,
733
+ draw_outlines=draw_outlines,
734
+ )
735
+
736
+ return {
737
+ "n_bytes": int(n_bytes),
738
+ "block_len": int(block_len),
739
+ "n_blocks": int(n_blocks),
740
+ "n_bands": int(n_bands),
741
+ "base_step": float(base_step),
742
+ "mu": float(mu),
743
+ "payload_len": int(payload_len),
744
+ "out_path": out_path,
745
+ "unzip_gif": unzip_gif,
746
+ "bands_applied": int(bands_to_apply),
747
+ }
748
+
749
+ # --------------------------
750
+ # Quality metric
751
+ # --------------------------
752
+ def cosine_similarity_bytes(a: bytes, b: bytes) -> float:
753
+ x = np.frombuffer(a, dtype=np.uint8).astype(np.float64)
754
+ y = np.frombuffer(b, dtype=np.uint8).astype(np.float64)
755
+ n = min(x.size, y.size)
756
+ x = x[:n]; y = y[:n]
757
+ x -= x.mean(); y -= y.mean()
758
+ nx = np.linalg.norm(x) + 1e-12
759
+ ny = np.linalg.norm(y) + 1e-12
760
+ return float(np.dot(x, y) / (nx * ny))
761
+
762
+ # --------------------------
763
+ # CLI wrapper (call explicitly)
764
+ # --------------------------
765
+ def flc_cli(argv=None):
766
+ ap = argparse.ArgumentParser(description="FLC v1.3: Fibonacci-banded DCT + φ quant + Fibonacci coding + horizon ring + tile outlines")
767
+ ap.add_argument("inp", type=str)
768
+ ap.add_argument("out_flc", type=str)
769
+ ap.add_argument("preview_png", type=str)
770
+ ap.add_argument("unzip_gif", type=str)
771
+ ap.add_argument("out_recovered", type=str)
772
+ ap.add_argument("--block", type=int, default=1024)
773
+ ap.add_argument("--bands", type=int, default=10)
774
+ ap.add_argument("--step", type=float, default=0.004)
775
+ ap.add_argument("--no_center", action="store_true")
776
+ ap.add_argument("--horizon", type=int, default=None)
777
+ ap.add_argument("--gif_scale", type=int, default=2)
778
+ ap.add_argument("--no_ring", action="store_true")
779
+ ap.add_argument("--no_outlines", action="store_true")
780
+ args = ap.parse_args(argv)
781
+
782
+ enc = flc_encode_file(
783
+ args.inp, args.out_flc, args.preview_png, unzip_gif=args.unzip_gif,
784
+ block_len=args.block, n_bands=args.bands, base_step=args.step,
785
+ use_mean_center=(not args.no_center),
786
+ gif_scale=args.gif_scale,
787
+ gif_horizon=None,
788
+ draw_horizon=(not args.no_ring),
789
+ draw_outlines=(not args.no_outlines),
790
+ )
791
+ print("ENC:", enc)
792
+
793
+ dec = flc_decode_file(
794
+ args.out_flc, args.out_recovered,
795
+ unzip_gif="decode_" + os.path.basename(args.unzip_gif),
796
+ max_bands=args.horizon,
797
+ gif_scale=args.gif_scale,
798
+ draw_horizon=(not args.no_ring),
799
+ draw_outlines=(not args.no_outlines),
800
+ )
801
+ print("DEC:", dec)
802
+
803
+ a = open(args.inp, "rb").read()
804
+ b = open(args.out_recovered, "rb").read()
805
+ cs = cosine_similarity_bytes(a, b)
806
+ print("Cosine similarity (bytes):", cs, " => ", cs*100, "%")
807
+ print("ORIG:", os.path.getsize(args.inp), "FLC:", os.path.getsize(args.out_flc),
808
+ "RATIO:", os.path.getsize(args.out_flc)/max(1, os.path.getsize(args.inp)))
809
+
810
+ # ============================================================
811
+ # Notebook demo (run manually)
812
+ # ============================================================
813
+ with open("test_input.bin","wb") as f:
814
+ f.write(b"Black hole horizon unzip meets Fibonacci spiral. " * 220)
815
+ enc = flc_encode_file("test_input.bin","test_output.flc","test_preview.png",
816
+ unzip_gif="test_unzip.gif",
817
+ block_len=1024, n_bands=10, base_step=0.004,
818
+ gif_scale=2, draw_horizon=True, draw_outlines=True)
819
+ print("ENC:", enc)
820
+ dec = flc_decode_file("test_output.flc","test_recovered.bin",
821
+ unzip_gif="test_decode_unzip.gif",
822
+ max_bands=None, gif_scale=2,
823
+ draw_horizon=True, draw_outlines=True)
824
+ print("DEC:", dec)
825
+ a = open("test_input.bin","rb").read()
826
+ b = open("test_recovered.bin","rb").read()
827
+ print("Cosine similarity:", cosine_similarity_bytes(a,b))
828
+ decp = flc_decode_file("test_output.flc","test_partial.bin",
829
+ unzip_gif="test_partial_unzip.gif",
830
+ max_bands=4, gif_scale=2,
831
+ draw_horizon