forked from vycdev/thermal-printer-scripts
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathattractor_print.py
More file actions
118 lines (98 loc) · 4.37 KB
/
Copy pathattractor_print.py
File metadata and controls
118 lines (98 loc) · 4.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#!/usr/bin/env python3
"""Print a strange attractor on the S01 thermal printer.
Iterates a chaotic 2D map (Clifford or de Jong) a few hundred thousand times and
accumulates how often each pixel is visited; the density is mapped to gray and
dithered, tracing out the attractor's wispy structure. Each seed picks new
parameters, so every print is a different creature.
Examples:
python attractor_print.py # random Clifford attractor
python attractor_print.py --type dejong
python attractor_print.py --seed 1234
python attractor_print.py --iters 400000 # denser (slower)
python attractor_print.py --no-print
"""
from __future__ import annotations
import argparse
from array import array
from math import cos, log, sin
from pathlib import Path
import random
from PIL import Image
from print_common import ROOT, Card
def iterate(kind: str, a, b, c, d, iters: int):
xs, ys = array("d"), array("d")
x = y = 0.1
for i in range(iters):
if kind == "dejong":
x, y = sin(a * y) - cos(b * x), sin(c * x) - cos(d * y)
else: # clifford
x, y = sin(a * y) + c * cos(a * x), sin(b * x) + d * cos(b * y)
if i > 1000: # let the orbit settle onto the attractor
xs.append(x)
ys.append(y)
return xs, ys
def coverage(xs, ys, size: int = 200) -> float:
"""Fraction of grid cells the orbit visits -- sparse/degenerate orbits score low."""
minx, maxx, miny, maxy = min(xs), max(xs), min(ys), max(ys)
s = (size - 1) / max(maxx - minx, maxy - miny, 1e-9)
seen = set()
for x, y in zip(xs, ys):
seen.add((int((x - minx) * s), int((y - miny) * s)))
return len(seen) / (size * size)
def find_params(kind: str, rng: random.Random):
"""Probe random parameters cheaply and keep the first that fills out nicely."""
best, best_cov = None, -1.0
for _ in range(20):
p = tuple(round(rng.uniform(-2, 2), 3) for _ in range(4))
xs, ys = iterate(kind, *p, 45000)
cov = coverage(xs, ys)
if cov > best_cov:
best, best_cov = p, cov
if cov >= 0.05:
break
return best, best_cov
def render(xs, ys, size: int) -> Image.Image:
minx, maxx = min(xs), max(xs)
miny, maxy = min(ys), max(ys)
sx = (size - 1) / (maxx - minx or 1)
sy = (size - 1) / (maxy - miny or 1)
s = min(sx, sy)
ox = (size - (maxx - minx) * s) / 2
oy = (size - (maxy - miny) * s) / 2
dens = array("I", [0]) * (size * size)
for x, y in zip(xs, ys):
col = int(ox + (x - minx) * s)
row = int(oy + (y - miny) * s)
dens[row * size + col] += 1
peak = max(dens)
lp = log(1 + peak)
buf = bytearray(size * size)
for i, dcount in enumerate(dens):
buf[i] = 255 if dcount == 0 else int(255 * (1 - log(1 + dcount) / lp))
return Image.frombytes("L", (size, size), bytes(buf))
def main() -> int:
parser = argparse.ArgumentParser(description="Print a strange attractor on the S01 thermal printer.")
parser.add_argument("--type", choices=["clifford", "dejong"], default="clifford")
parser.add_argument("--seed", type=int, default=None, help="Seed for the parameters.")
parser.add_argument("--iters", type=int, default=260000, help="Iteration count (default: 260000).")
parser.add_argument("--out", type=Path, default=None)
parser.add_argument("--darkness", type=int, choices=range(1, 6), default=3)
parser.add_argument("--bottom-feed", type=int, default=24)
parser.add_argument("--no-print", action="store_true")
args = parser.parse_args()
seed = args.seed if args.seed is not None else random.randrange(1, 100000)
rng = random.Random(seed)
(a, b, c, d), cov = find_params(args.type, rng)
print(f"{args.type} attractor seed {seed}: a={a} b={b} c={c} d={d} (coverage {cov:.1%}), {args.iters} iters ...")
card = Card()
xs, ys = iterate(args.type, a, b, c, d, args.iters)
img = render(xs, ys, card.inner_w)
card.title("STRANGE ATTRACTOR")
card.line(args.type, size=12, bold=True, center=True)
card.gap(4).image(img, border=True)
card.line(f"a={a} b={b} c={c} d={d}", size=10, bold=False, center=True)
card.footer("chaos", f"seed {seed}")
out = args.out or ROOT / f"attractor_{args.type}_{seed}.png"
return card.finish(out, args.bottom_feed, args.darkness, do_print=not args.no_print)
if __name__ == "__main__":
raise SystemExit(main())