"""The ceiling nobody measured: can a modality identify a scene against itself? Every deficit so far has been read as a cross-modal one, and that assumed each side describes its scenes well enough to identify them. Nothing tested the assumption. Split each scene's parts into two disjoint halves -- odd and even regions, odd and even segments -- build a relation field from each half, and match the halves against each other with the anchor bound. Both halves come from the same modality and the same scene, so any failure is that modality failing to pin down its own scenes. This upper-bounds anything cross-modal. If one half of an image's description cannot pick that image out from among the others given the other half, no vision encoder can be blamed for failing to, and no better cross-modal recipe exists to find. It costs one pass over data already on disk, and it should have been the first measurement on natural data rather than one of the last. """ from __future__ import annotations import argparse import json import re from pathlib import Path import numpy as np import torch import torch.nn.functional as F from scipy.optimize import linear_sum_assignment from .common import seed_everything, write_json from .natural_families import load_phrases from .natural_pipeline import content_directions, word_vectors from .synth_cc_battery import moment_field def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--vg-dir", default="artifacts/vg_5k") parser.add_argument("--objects", default="artifacts/vg_5k/nobj_base_gpu.pt") parser.add_argument("--samples", type=int, default=256) parser.add_argument("--fit-scenes", type=int, default=1200) parser.add_argument("--word-vectors", type=int, default=128) parser.add_argument("--min-count", type=int, default=60) parser.add_argument("--max-vocabulary", type=int, default=600) parser.add_argument("--context-window", type=int, default=2) parser.add_argument("--keep", type=int, default=64) parser.add_argument("--shrinkage", type=float, default=0.05) parser.add_argument("--weight-power", type=float, default=0.5) parser.add_argument("--seed", type=int, default=0) parser.add_argument("--output", default="artifacts/vg_5k/split_half.json") return parser.parse_args() def standardise(matrix: np.ndarray) -> np.ndarray: values = matrix[~np.eye(len(matrix), dtype=bool)] out = (matrix - values.mean()) / values.std() np.fill_diagonal(out, 0.0) return out def anchor_bound(first: np.ndarray, second: np.ndarray, repeats: int = 5) -> float: size = len(first) scores = [] for repeat in range(repeats): generator = np.random.default_rng(repeat) order = generator.permutation(size) half = size // 2 anchors, probe = order[:half], order[half:] def profile(field): block = field[np.ix_(probe, anchors)] block = block - block.mean(1, keepdims=True) return block / block.std(1, keepdims=True).clip(1e-9) similarity = profile(first) @ profile(second).T / half _, columns = linear_sum_assignment(-similarity) scores.append(float((columns == np.arange(len(probe))).mean())) return float(np.mean(scores)) def build_field(sets, fitted, keep, power) -> torch.Tensor: centre, basis, values = fitted width = min(keep, basis.shape[1]) scale = values[:width] ** power if power else 1.0 projected = [ F.normalize( torch.tensor((item - centre) @ basis[:, :width] * scale, dtype=torch.float32), dim=-1, ) for item in sets ] return moment_field(projected) def main() -> None: args = parse_args() seed_everything(args.seed) vg_dir = Path(args.vg_dir) truth = [ json.loads(line) for line in (vg_dir / "ground_truth.private.jsonl").read_text(encoding="utf-8").splitlines() if line.strip() ] records = { json.loads(line)["node_id"]: json.loads(line) for line in (vg_dir / "text_nodes.jsonl").read_text(encoding="utf-8").splitlines() if line.strip() } state = torch.load(args.objects, map_location="cpu", weights_only=False) index = {node: position for position, node in enumerate(state["node_ids"])} vectors = word_vectors(load_phrases(vg_dir / "text_nodes.jsonl", "region_closed"), args) def text_parts(node): rows = [] for phrase in records[node]["region_closed"]: hits = [vectors[t] for t in re.findall(r"[a-z]+", phrase.lower()) if t in vectors] if hits: rows.append(np.mean(hits, axis=0)) return rows def vision_parts(node): return [s["feature"].astype(np.float64) for s in state["segments"][index[node]]] pairs = [p for p in truth if p["vision_node_id"] in index and p["text_node_id"] in records] def collect(getter, key, rows, minimum): out = [] for pair in rows: parts = getter(pair[key]) out.append(np.stack(parts) if len(parts) >= minimum else None) return out results = {} for name, getter, key in ( ("text", text_parts, "text_node_id"), ("vision", vision_parts, "vision_node_id"), ): fit_rows = pairs[args.samples : args.samples + args.fit_scenes] fit = [x for x in collect(getter, key, fit_rows, 2) if x is not None] fitted = content_directions(fit, args.shrinkage) evaluated, first_half, second_half = [], [], [] for pair in pairs[: args.samples]: parts = getter(pair[key]) if len(parts) < 4: continue stacked = np.stack(parts) evaluated.append(pair) first_half.append(stacked[0::2]) second_half.append(stacked[1::2]) field_a = standardise( build_field(first_half, fitted, args.keep, args.weight_power).double().numpy() ) field_b = standardise( build_field(second_half, fitted, args.keep, args.weight_power).double().numpy() ) mask = ~np.eye(len(evaluated), dtype=bool) results[name] = { "scenes": len(evaluated), "mean_parts": float(np.mean([len(getter(p[key])) for p in evaluated])), "split_half_correlation": float( np.corrcoef(field_a[mask], field_b[mask])[0, 1] ), "split_half_anchor_bound": anchor_bound(field_a, field_b), } print( f"{name:<7} scenes={results[name]['scenes']:4d} " f"parts={results[name]['mean_parts']:5.1f} " f"rho={results[name]['split_half_correlation']:.3f} " f"self-identification bound={results[name]['split_half_anchor_bound']:.3f}", flush=True, ) summary = { "protocol": ( "Each scene's parts split into disjoint halves within one modality; " "a relation field is built from each half and the two are matched by " "the anchor bound. Both halves describe the same scenes, so this " "upper-bounds any cross-modal result." ), "results": results, "cross_modal_bound_for_reference": 0.291, } print(json.dumps(summary["results"])) write_json(args.output, summary) if __name__ == "__main__": main()