"""PixelModel v3 - architecture + PNG weight codec. A text-to-image CPPN whose entire weight set is stored as pixels in a PNG. v3 changes relative to v1/v2 (see README): 1. Learned word-embedding table (mean-pooled) replaces the 0-param hash embed. 2. SIREN (sine) decoder with the real SIREN init instead of tanh. 3. Wider/deeper decoder (128-256 wide, 4-5 layers). 4. FiLM conditioning (per-layer gamma/beta from z) replaces concat conditioning. 5. PNG weight-encoding scheme extended to the new param count. 6/7. Fully batched coordinate decoding (no per-pixel python loop). The PNG codec is deliberately identical in spirit to v1: one weight per pixel, 16-bit precision spread across the R (high byte) and G (low byte) channels, B reserved, values linearly mapped from a fixed [-2, 2] range. """ from __future__ import annotations import json import math import os import time from dataclasses import dataclass, asdict, field import numpy as np try: import torch import torch.nn as nn import torch.nn.functional as F except Exception: torch = None nn = object WEIGHT_RANGE = (-2.0, 2.0) PAD_ID = 0 UNK_ID = 1 @dataclass class ModelConfig: vocab_size: int = 8192 embed_dim: int = 64 max_tokens: int = 20 text_hidden: int = 256 z_dim: int = 192 decoder_width: int = 256 num_sine_layers: int = 4 num_freq: int = 8 w0_first: float = 30.0 w0_hidden: float = 30.0 weight_range: tuple = WEIGHT_RANGE @property def coord_dim(self) -> int: return 2 + 4 * self.num_freq def to_json(self) -> dict: d = asdict(self) d["weight_range"] = list(self.weight_range) d["coord_dim"] = self.coord_dim return d @staticmethod def from_json(d: dict) -> "ModelConfig": keys = {f for f in ModelConfig.__dataclass_fields__} clean = {k: v for k, v in d.items() if k in keys} if "weight_range" in clean: clean["weight_range"] = tuple(clean["weight_range"]) return ModelConfig(**clean) import re as _re _TOKEN_RE = _re.compile(r"[a-z0-9]+") def simple_tokenize(text: str) -> list[str]: """Lowercase, split on non-alphanumeric runs. Deterministic and reversible enough for a bag-of-words embedding.""" return _TOKEN_RE.findall(text.lower()) def build_vocab(captions, vocab_size: int, min_freq: int = 1) -> dict: """captions: iterable of strings -> {token: id}. Ids 0/1 are PAD/UNK.""" from collections import Counter counter = Counter() for cap in captions: counter.update(simple_tokenize(cap)) vocab = {"": PAD_ID, "": UNK_ID} for tok, freq in counter.most_common(): if len(vocab) >= vocab_size: break if freq < min_freq: break if tok not in vocab: vocab[tok] = len(vocab) return vocab def load_vocab(path: str) -> dict: with open(path) as f: return json.load(f) def encode_caption(text: str, vocab: dict, max_tokens: int) -> np.ndarray: """text -> int32 array of length max_tokens (PAD-filled, UNK-mapped).""" ids = [vocab.get(t, UNK_ID) for t in simple_tokenize(text)][:max_tokens] out = np.full(max_tokens, PAD_ID, dtype=np.int32) out[:len(ids)] = ids return out def fourier_features(coords, num_freq: int): """coords: (..., 2) in [-1, 1] -> (..., 2 + 4*num_freq). One explicit batched tensor op over the whole pixel set; no python loop over pixels. Frequencies are the octaves pi * 2^k, k = 0..num_freq-1. """ freqs = (2.0 ** torch.arange(num_freq, device=coords.device, dtype=coords.dtype)) * math.pi scaled = coords.unsqueeze(-1) * freqs sin = torch.sin(scaled) cos = torch.cos(scaled) enc = torch.cat([coords, sin.flatten(-2), cos.flatten(-2)], dim=-1) return enc class FiLMSineLayer(nn.Module): """Linear -> FiLM(gamma, beta) -> sin(w0 * .). FiLM is applied *after* the linear and *before* the sine, so the conditioning warps the pre-activation the same way a learned bias would, while the SIREN frequency scaling still governs the activation spectrum. """ def __init__(self, in_features: int, out_features: int, w0: float, is_first: bool): super().__init__() self.in_features = in_features self.out_features = out_features self.w0 = w0 self.is_first = is_first self.linear = nn.Linear(in_features, out_features) self.siren_init() def siren_init(self): with torch.no_grad(): if self.is_first: bound = 1.0 / self.in_features else: bound = math.sqrt(6.0 / self.in_features) / self.w0 self.linear.weight.uniform_(-bound, bound) if self.linear.bias is not None: self.linear.bias.uniform_(-bound, bound) def forward(self, x, gamma, beta): h = self.linear(x) h = gamma * h + beta return torch.sin(self.w0 * h) class PixelModelV3(nn.Module): def __init__(self, cfg: ModelConfig): super().__init__() self.cfg = cfg self.embed = nn.Embedding(cfg.vocab_size, cfg.embed_dim, padding_idx=PAD_ID) nn.init.normal_(self.embed.weight, std=0.05) with torch.no_grad(): self.embed.weight[PAD_ID].zero_() self.text_fc1 = nn.Linear(cfg.embed_dim, cfg.text_hidden) self.text_fc2 = nn.Linear(cfg.text_hidden, cfg.z_dim) self.film = nn.Linear(cfg.z_dim, cfg.num_sine_layers * 2 * cfg.decoder_width) nn.init.zeros_(self.film.weight) nn.init.zeros_(self.film.bias) layers = [] in_dim = cfg.coord_dim for i in range(cfg.num_sine_layers): is_first = (i == 0) w0 = cfg.w0_first if is_first else cfg.w0_hidden layers.append(FiLMSineLayer(in_dim, cfg.decoder_width, w0=w0, is_first=is_first)) in_dim = cfg.decoder_width self.sine_layers = nn.ModuleList(layers) self.head = nn.Linear(cfg.decoder_width, 3) with torch.no_grad(): bound = math.sqrt(6.0 / cfg.decoder_width) / cfg.w0_hidden self.head.weight.uniform_(-bound, bound) nn.init.zeros_(self.head.bias) self.num_freq = cfg.num_freq self.grad_checkpoint = False def encode_text(self, token_ids): """token_ids: (B, L) long -> z: (B, z_dim).""" emb = self.embed(token_ids) mask = (token_ids != PAD_ID).float().unsqueeze(-1) summed = (emb * mask).sum(dim=1) count = mask.sum(dim=1).clamp(min=1.0) pooled = summed / count h = torch.sin(self.text_fc1(pooled)) z = self.text_fc2(h) return z def film_params(self, z): """z: (B, z_dim) -> list of (gamma, beta), each (B, 1, width).""" raw = self.film(z) raw = raw.view(-1, self.cfg.num_sine_layers, 2, self.cfg.decoder_width) out = [] for i in range(self.cfg.num_sine_layers): gamma = 1.0 + raw[:, i, 0, :].unsqueeze(1) beta = raw[:, i, 1, :].unsqueeze(1) out.append((gamma, beta)) return out def decode(self, coords, z, return_stats: bool = False): """coords: (B, N, 2) in [-1, 1]; z: (B, z_dim) -> rgb (B, N, 3). The whole pixel set N is decoded as one batched tensor op per layer. """ x = fourier_features(coords, self.num_freq) films = self.film_params(z) use_ckpt = self.grad_checkpoint and self.training and not return_stats and x.requires_grad stats = [] for layer, (gamma, beta) in zip(self.sine_layers, films): if use_ckpt: x = torch.utils.checkpoint.checkpoint(layer, x, gamma, beta, use_reentrant=False) else: x = layer(x, gamma, beta) if return_stats: stats.append((float(x.mean()), float(x.std()))) rgb = torch.sigmoid(self.head(x)) if return_stats: return rgb, stats return rgb def forward(self, token_ids, coords, return_stats: bool = False): z = self.encode_text(token_ids) return self.decode(coords, z, return_stats=return_stats) def make_coord_grid(height: int, width: int, device=None, dtype=None): """-> (H*W, 2) coordinate grid in [-1, 1]. One tensor op, no python loop.""" ys = torch.linspace(-1.0, 1.0, height, device=device, dtype=dtype) xs = torch.linspace(-1.0, 1.0, width, device=device, dtype=dtype) gy, gx = torch.meshgrid(ys, xs, indexing="ij") return torch.stack([gx, gy], dim=-1).reshape(-1, 2) def param_breakdown(model: "PixelModelV3") -> dict: """Per-parameter-tensor breakdown, in the exact order used by the PNG codec.""" breakdown = [] total = 0 for name, p in model.named_parameters(): n = int(p.numel()) breakdown.append({"name": name, "shape": list(p.shape), "params": n}) total += n return {"total_parameters": total, "param_breakdown": breakdown} def png_dims_for(n_params: int) -> tuple[int, int]: """Roughly-square PNG that holds n_params pixels (one weight per pixel).""" width = int(math.ceil(math.sqrt(n_params))) height = int(math.ceil(n_params / width)) return width, height def flatten_params(model: "PixelModelV3") -> np.ndarray: chunks = [p.detach().cpu().float().reshape(-1).numpy() for p in model.parameters()] return np.concatenate(chunks) def load_flat_params(model: "PixelModelV3", flat: np.ndarray) -> None: i = 0 for p in model.parameters(): n = p.numel() chunk = torch.from_numpy(np.ascontiguousarray(flat[i:i + n])).to(p.dtype).view_as(p) p.data.copy_(chunk) i += n if i != flat.shape[0]: raise ValueError(f"param/flat size mismatch: consumed {i}, have {flat.shape[0]}") def weights_to_png(flat: np.ndarray, path: str, weight_range=WEIGHT_RANGE): """Write a float weight vector to a PNG. Returns (width, height, n_clamped).""" from PIL import Image n = int(flat.size) width, height = png_dims_for(n) total = width * height padded = np.zeros(total, dtype=np.float64) padded[:n] = flat.astype(np.float64) lo, hi = weight_range n_clamped = int(np.count_nonzero((padded[:n] < lo) | (padded[:n] > hi))) clamped = np.clip(padded, lo, hi) u16 = np.round((clamped - lo) / (hi - lo) * 65535.0).astype(np.uint16) img = np.zeros((height, width, 3), dtype=np.uint8) img[..., 0] = (u16 >> 8).astype(np.uint8).reshape(height, width) img[..., 1] = (u16 & 0xFF).astype(np.uint8).reshape(height, width) Image.fromarray(img, "RGB").save(path, optimize=True) return width, height, n_clamped def png_to_weights(path: str, n_params: int, weight_range=WEIGHT_RANGE) -> np.ndarray: """Decode a PNG back into the first n_params weight values (float32).""" from PIL import Image arr = np.asarray(Image.open(path).convert("RGB")) hi_byte = arr[..., 0].astype(np.uint16) lo_byte = arr[..., 1].astype(np.uint16) u16 = ((hi_byte << 8) | lo_byte).reshape(-1)[:n_params] lo, hi = weight_range vals = u16.astype(np.float64) / 65535.0 * (hi - lo) + lo return vals.astype(np.float32) def save_model_png(model: "PixelModelV3", png_path: str, config_path: str | None = None, verbose: bool = True) -> dict: """Write model.png (and optionally config.json). Returns a stats dict.""" flat = flatten_params(model) n = int(flat.size) t0 = time.time() width, height, n_clamped = weights_to_png(flat, png_path, model.cfg.weight_range) write_s = time.time() - t0 size_bytes = os.path.getsize(png_path) info = param_breakdown(model) info.update({ "png_width": width, "png_height": height, "png_pixels": width * height, "png_bytes": size_bytes, "png_write_seconds": round(write_s, 4), "weights_clamped": n_clamped, "config": model.cfg.to_json(), }) if config_path is not None: with open(config_path, "w") as f: json.dump(info, f, indent=2) if verbose: print(f"[png] total params : {n:,}") print(f"[png] resolution : {width} x {height} ({width*height:,} pixels)") print(f"[png] file size on disk : {size_bytes/1024:.1f} KiB ({size_bytes/1e6:.3f} MB)") print(f"[png] write time : {write_s*1000:.1f} ms") if n_clamped: frac = 100.0 * n_clamped / n print(f"[png] WARNING clamped : {n_clamped:,} weights ({frac:.3f}%) outside {model.cfg.weight_range}") if size_bytes > 4_000_000: print(f"[png] WARNING size : {size_bytes/1e6:.2f} MB is larger than expected") return info def load_model_png(png_path: str, cfg: ModelConfig, map_location="cpu", verbose: bool = True) -> "PixelModelV3": """Rebuild the model architecture from cfg, then fill weights from the PNG.""" model = PixelModelV3(cfg) n_params = sum(p.numel() for p in model.parameters()) t0 = time.time() flat = png_to_weights(png_path, n_params, cfg.weight_range) load_flat_params(model, flat) model.to(map_location) model.eval() if verbose: print(f"[png] loaded {n_params:,} weights from {png_path} in {(time.time()-t0)*1000:.1f} ms") return model def load_config(config_path: str) -> ModelConfig: with open(config_path) as f: info = json.load(f) cfg_dict = info.get("config", info) return ModelConfig.from_json(cfg_dict) def roundtrip_error(model: "PixelModelV3", tmp_png: str) -> float: """Max abs weight error after a PNG encode/decode round trip.""" before = flatten_params(model) weights_to_png(before, tmp_png, model.cfg.weight_range) after = png_to_weights(tmp_png, before.size, model.cfg.weight_range) return float(np.max(np.abs(before - after)))