File size: 11,878 Bytes
41dd7e0 | 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 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 | """Shared pieces of the appendix diagnostics: the inputs of one system, exact spectral transforms of coarse states,
and the joint extraction of the pool3 (cFID) and spatial (csFID) Inception features."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import numpy as np
from bifurcation.config import RECORDS, SYSTEMS, data_paths, dataset_card, evaluation_settings, records
METHODS = ("direct_si", "edm", "cascaded_si")
LABELS = {"direct_si": "Direct SI", "edm": "EDM", "cascaded_si": "Cascaded SI"}
def add_system_args(ap: argparse.ArgumentParser, samples: bool = True) -> None:
ap.add_argument("--system", required=True, choices=SYSTEMS)
ap.add_argument("--data", default=None, help="dataset package directory (default: $<SYSTEM>_DATA)")
ap.add_argument("--truth-bank", default=None, help="truth-bank directory (default: $<SYSTEM>_TRUTH_BANK)")
if samples:
ap.add_argument("--samples", action="append", default=[], metavar="METHOD=DIR",
help="test samples of a Table 2 model (direct_si, edm, cascaded_si); repeatable")
ap.add_argument("--out", type=Path, required=True)
ap.add_argument("--device", default="cuda")
class SystemInputs:
"""The package, truth bank, image adapter, normalization and recorded models of one system."""
def __init__(self, a):
from bifurcation.fid.adapters import FIDAdapter
self.system = a.system
self.data, self.bank = data_paths(a.system, a.data, a.truth_bank)
self.card = dataset_card(a.system)
self.rec = records(a.system)
self.fid_cfg = evaluation_settings()["fid"]
self.adapter = FIDAdapter.load(RECORDS / a.system / "fid_adapter.json", **(self.card["fid_adapter"].get("params") or {}))
z = np.load(self.data / "normalization.npz")
self.nmean, self.nstd = np.asarray(z["mean"], np.float64), np.asarray(z["std"], np.float64)
self.t_idx = int(self.rec["fine_horizon_idx"])
self.gids = sorted(int(g) for g in self.rec["test_gids"])
self.samples = {}
for s in getattr(a, "samples", []) or []:
m, _, d = s.partition("=")
if m not in METHODS or not d:
raise SystemExit(f"--samples expects METHOD=DIR with METHOD in {METHODS}")
self.samples[m] = Path(d).resolve()
def unit(self, method: str) -> dict:
key = self.rec["table2"][method]
return next(u for u in self.rec["units"] if u["key"] == key)
def generated(self, method: str, gid: int, n: int) -> np.ndarray:
"""RAW generated states (n, D) of test condition ``gid`` from the samples of ``method``."""
u = self.unit(method)
d = self.samples.get(method)
if d is None and method == "cascaded_si" and self.rec["table2"]["cascaded_si"] == self.rec["table2"]["direct_si"]:
d = self.samples.get("direct_si") # S* = 1: the same model and samples as Direct SI
if d is None:
raise SystemExit(f"--samples {method}=DIR is needed")
p = d / u["sampler"]["variant"] / f"group_{gid:04d}.npz"
if not p.is_file():
raise FileNotFoundError(f"{method}: no samples of condition {gid} at {p}")
with np.load(p) as z:
return np.asarray(z["gen_xT_norm"], np.float64)[:n] * self.nstd + self.nmean
def models(self) -> list[dict]:
"""The distinct Table 2 models with samples, in the order direct_si, edm, cascaded_si (a shared model once)."""
out, seen = [], {}
for m in METHODS:
key = self.rec["table2"][m]
if key in seen:
seen[key]["methods"].append(m); continue
if m not in self.samples and not (m == "cascaded_si" and key == self.rec["table2"]["direct_si"] and "direct_si" in self.samples):
continue
seen[key] = {"key": key, "methods": [m], "n_steps": int(self.unit(m)["sampler"]["n_steps"])}
out.append(seen[key])
for m in out:
m["label"] = "/".join(LABELS[x] for x in m["methods"])
return out
# ββ exact transforms of coarse spectral states (no interpolation) ββββββββββββββββββββββββββββββββββββββββββββββ
def shifted(coeff_raw, kx, ky, dx, dy, n_disp):
"""Periodic translation by (dx, dy) pixels of the n_disp grid: a phase factor on every mode."""
M = len(kx)
c = coeff_raw[:, :M] + 1j * coeff_raw[:, M:]
ph = np.exp(-2j * np.pi * (kx * dx + ky * dy) / n_disp)
c = c * ph
return np.concatenate([c.real, c.imag], axis=1)
def rot90(coeff_raw, kx, ky, ix):
"""Rotation by ix * 90 degrees: a permutation of the Hermitian-half modes (conjugated where R k leaves the half)."""
M = len(kx)
c = coeff_raw[:, :M] + 1j * coeff_raw[:, M:]
key = {(int(a), int(b)): i for i, (a, b) in enumerate(zip(kx, ky))}
out = np.zeros_like(c)
for i, (a, b) in enumerate(zip(kx, ky)):
for _ in range(ix):
a, b = -b, a
if (int(a), int(b)) in key:
out[:, key[(int(a), int(b))]] = c[:, i]
elif (-int(a), -int(b)) in key:
out[:, key[(-int(a), -int(b))]] = np.conj(c[:, i])
else:
raise KeyError((a, b))
return np.concatenate([out.real, out.imag], axis=1)
def flip(coeff_raw, kx, ky, axis):
"""Reflection k_x -> -k_x (axis 0) or k_y -> -k_y (axis 1), with the Hermitian partner conjugated."""
M = len(kx)
c = coeff_raw[:, :M] + 1j * coeff_raw[:, M:]
key = {(int(a), int(b)): i for i, (a, b) in enumerate(zip(kx, ky))}
out = np.zeros_like(c)
for i, (a, b) in enumerate(zip(kx, ky)):
a2, b2 = (-int(a), int(b)) if axis == 0 else (int(a), -int(b))
if (a2, b2) in key:
out[:, key[(a2, b2)]] = c[:, i]
else:
out[:, key[(-a2, -b2)]] = np.conj(c[:, i])
return np.concatenate([out.real, out.imag], axis=1)
# ββ cFID and csFID features from one forward pass ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
SFID_CHANNELS = 7
SFID_SPATIAL = 17
SFID_DIM = SFID_SPATIAL * SFID_SPATIAL * SFID_CHANNELS # 2023
CFID_DIM = 2048
CANON_MIXED5_SHAPE = (768, SFID_SPATIAL, SFID_SPATIAL)
def _stages(L):
pre = [L.conv, L.conv_1, L.conv_2, L.pool0, L.conv_3, L.conv_4, L.pool1,
L.mixed, L.mixed_1, L.mixed_2, L.mixed_3, L.mixed_4, L.mixed_5]
post = [L.mixed_6, L.mixed_7, L.mixed_8, L.mixed_9, L.mixed_10, L.pool2]
return pre, post
def verify_layer_identity(extractor, device, cpu_extractor=None) -> dict:
"""Check that the stepwise traversal reproduces the pool3 feature (bit-exactly on CPU, so the spatial tap sits on
the cFID path) and that the tapped tensor is the 17 x 17 mixed_6/conv activation. Returns the record."""
import torch
L = extractor.layers
if not hasattr(L, "mixed_5") or not hasattr(L, "mixed_6") or not hasattr(L.mixed_6, "conv"):
raise RuntimeError("this Inception build does not expose layers.mixed_6.conv")
pre, post = _stages(L)
x = (torch.rand(3, 3, 299, 299, device=device) * 255.0 - 128) / 128
with torch.no_grad():
h = x
for st in pre:
h = st.forward(h)
mixed5 = h
spatial = L.mixed_6.conv.forward(mixed5)
for st in post:
h = st.forward(h)
gpu_diff = float((h.view(3, CFID_DIM) - L.forward(x).view(3, CFID_DIM)).abs().max())
cpu_diff = None
if cpu_extractor is not None:
Lc = cpu_extractor.layers
pre_c, post_c = _stages(Lc)
xc = (torch.rand(3, 3, 299, 299) * 255.0 - 128) / 128
with torch.no_grad():
hc = xc
for st in pre_c:
hc = st.forward(hc)
for st in post_c:
hc = st.forward(hc)
cpu_diff = float((hc.view(3, CFID_DIM) - Lc.forward(xc).view(3, CFID_DIM)).abs().max())
flat = spatial[:, :SFID_CHANNELS].permute(0, 2, 3, 1).reshape(3, -1)
rec = {"submodule": "layers.mixed_6.conv", "mixed_5_output_shape_CHW": list(mixed5.shape[1:]),
"mixed_6_conv_output_shape_CHW": list(spatial.shape[1:]), "channels_total": int(spatial.shape[1]),
"channels_kept": SFID_CHANNELS, "spatial_kept": [int(spatial.shape[2]), int(spatial.shape[3])],
"feature_dim": int(flat.shape[1]), "flatten_order": "NHWC",
"split_traversal_matches_layers_forward_bitexact_cpu": bool(cpu_diff == 0.0) if cpu_diff is not None else None,
"split_traversal_max_abs_diff_cpu": cpu_diff, "split_traversal_max_abs_diff_cuda": gpu_diff}
problems = []
if tuple(mixed5.shape[1:]) != CANON_MIXED5_SHAPE:
problems.append(f"mixed_5 output {tuple(mixed5.shape[1:])} != {CANON_MIXED5_SHAPE}")
if tuple(spatial.shape[2:]) != (SFID_SPATIAL, SFID_SPATIAL):
problems.append(f"mixed_6/conv spatial {tuple(spatial.shape[2:])} != {SFID_SPATIAL}x{SFID_SPATIAL}")
if int(flat.shape[1]) != SFID_DIM:
problems.append(f"feature dim {int(flat.shape[1])} != {SFID_DIM}")
if cpu_diff is not None and cpu_diff != 0.0:
problems.append(f"CPU split traversal differs from layers.forward by {cpu_diff}")
if gpu_diff > 1e-2:
problems.append(f"device split traversal differs from layers.forward by {gpu_diff}")
rec["problems"] = problems
if problems:
raise RuntimeError("spatial feature verification failed: " + "; ".join(problems))
print(f"Inception features verified: mixed_6/conv {list(spatial.shape[1:])}, {SFID_CHANNELS} channels kept -> "
f"{int(flat.shape[1])}-D; stepwise traversal vs layers.forward: cpu {cpu_diff}, device {gpu_diff}\n")
return rec
def both_features(imgs_u8, extractor, device, batch: int = 200):
"""uint8 (N, H, W, 3) -> (pool3 features (N, 2048), spatial features (N, 2023)) from one forward pass; the same
resize and normalization as bifurcation.fid.backend."""
import torch
from cleanfid.resize import build_resizer
imgs_u8 = np.asarray(imgs_u8)
if imgs_u8.dtype != np.uint8 or imgs_u8.ndim != 4 or imgs_u8.shape[-1] != 3:
raise ValueError(f"adapter output must be uint8 (N, H, W, 3); got {imgs_u8.dtype} {imgs_u8.shape}")
L = extractor.layers
pre, _ = _stages(L)
resizer = build_resizer("clean")
fc, fs = [], []
with torch.no_grad():
for i in range(0, len(imgs_u8), int(batch)):
xs = [resizer(im) for im in imgs_u8[i:i + int(batch)]]
x = torch.from_numpy(np.stack(xs).transpose(0, 3, 1, 2).copy()).to(device)
if tuple(x.shape[-2:]) != (299, 299):
raise AssertionError("resizer did not produce 299x299")
fc.append(extractor(x).cpu().numpy())
h = (x - 128) / 128
for st in pre:
h = st.forward(h)
sp = L.mixed_6.conv.forward(h)[:, :SFID_CHANNELS]
fs.append(sp.permute(0, 2, 3, 1).reshape(sp.shape[0], -1).cpu().numpy())
return np.concatenate(fc).astype(np.float32), np.concatenate(fs).astype(np.float32)
def extractors(fid_cfg: dict, device: str):
"""(extractor on the device, the layer-identity record); the pins of the fid settings are checked first."""
import torch
from bifurcation.fid import backend as BE
BE.check_pins(fid_cfg)
dev = torch.device(device if torch.cuda.is_available() else "cpu")
ext = BE.build_extractor(dev)
cpu_ext = BE.build_extractor(torch.device("cpu")) if dev.type != "cpu" else ext
rec = verify_layer_identity(ext, dev, cpu_extractor=cpu_ext)
return ext, dev, rec
def write_json(path: Path, obj) -> None:
path.write_text(json.dumps(obj, indent=1, default=float) + "\n")
|