anhtld commited on
Commit
98a277b
·
verified ·
1 Parent(s): 29d22b0

codex rgb-ref visual-stat ctt artifacts 2026-07-03

Browse files
workspace/scripts/export_chart_observation_embeddings.py ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import hashlib
6
+ import io
7
+ import json
8
+ import subprocess
9
+ import sys
10
+ from collections import Counter
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
15
+ if str(PROJECT_ROOT) not in sys.path:
16
+ sys.path.insert(0, str(PROJECT_ROOT))
17
+
18
+ import numpy as np # noqa: E402
19
+
20
+ from cil.chart_features import OBSERVATION_EMBED_DIM # noqa: E402
21
+
22
+
23
+ def main(argv: list[str] | None = None) -> int:
24
+ parser = argparse.ArgumentParser(
25
+ description=(
26
+ "Decode chart observation_ref JPEGs and write deployment-visible "
27
+ "observation embeddings back into chart metadata."
28
+ )
29
+ )
30
+ parser.add_argument(
31
+ "--indexes",
32
+ nargs="+",
33
+ type=Path,
34
+ default=[
35
+ Path("data/cil_charts_rgb_refs/train/index.json"),
36
+ Path("data/cil_charts_rgb_refs/val/index.json"),
37
+ Path("data/cil_charts_rgb_refs/test/index.json"),
38
+ ],
39
+ )
40
+ parser.add_argument(
41
+ "--out-dir",
42
+ type=Path,
43
+ default=Path("runs/chart_observation_embeddings_rgb_refs"),
44
+ )
45
+ parser.add_argument("--overwrite", action="store_true")
46
+ args = parser.parse_args(argv)
47
+
48
+ out_dir = args.out_dir
49
+ out_dir.mkdir(parents=True, exist_ok=True)
50
+ _write_provenance(out_dir, args)
51
+
52
+ split_rows = []
53
+ for index_path in args.indexes:
54
+ split_rows.append(_process_index(index_path, overwrite=args.overwrite))
55
+
56
+ metrics = {
57
+ "report_type": "chart_observation_embedding_export",
58
+ "schema_version": 1,
59
+ "embedding_dim": OBSERVATION_EMBED_DIM,
60
+ "extractor": "rgb_jpeg_stats_v1",
61
+ "indexes": [str(path) for path in args.indexes],
62
+ "splits": split_rows,
63
+ "data_hash": {
64
+ row["split"]: row["content_hash_after"] for row in split_rows
65
+ },
66
+ "split_hash": {
67
+ row["split"]: row["split_hash"] for row in split_rows
68
+ },
69
+ "leakage_contract": {
70
+ "reads_outcomes": False,
71
+ "reads_observation_ref": True,
72
+ "writes_observation_embedding_path": True,
73
+ },
74
+ }
75
+ (out_dir / "metrics.json").write_text(json.dumps(metrics, indent=2, sort_keys=True) + "\n")
76
+ (out_dir / "metrics_by_task.json").write_text(_metrics_by_task(split_rows) + "\n")
77
+ (out_dir / "metrics_by_seed.json").write_text("{}\n")
78
+ (out_dir / "table.tex").write_text(_table(split_rows) + "\n")
79
+ (out_dir / "report.md").write_text(_report(metrics) + "\n")
80
+ (out_dir / "train.log").write_text("not a training run; exported observation embeddings\n")
81
+ (out_dir / "eval.log").write_text("decoded observation_ref JPEGs only; no outcomes read\n")
82
+ print(json.dumps({"out_dir": str(out_dir), "splits": len(split_rows)}, indent=2))
83
+ return 0
84
+
85
+
86
+ def _process_index(index_path: Path, *, overwrite: bool) -> dict[str, Any]:
87
+ index = json.loads(index_path.read_text())
88
+ split = str(index.get("split", index_path.parent.name))
89
+ embed_path = index_path.parent / "observation_embeddings_rgb_stats.npz"
90
+ if embed_path.exists() and not overwrite:
91
+ raise FileExistsError(f"{embed_path} exists; pass --overwrite to replace it")
92
+
93
+ ref_to_row: dict[tuple[str, str], int] = {}
94
+ embeddings: list[np.ndarray] = []
95
+ counters: Counter[str] = Counter()
96
+ task_counts: Counter[str] = Counter()
97
+ updated_shards: list[str] = []
98
+ h5_cache: dict[Path, Any] = {}
99
+ try:
100
+ for shard in index.get("shards", []):
101
+ shard_path = index_path.parent / str(shard["path"])
102
+ with np.load(shard_path, allow_pickle=False) as data:
103
+ arrays = {key: data[key] for key in data.files}
104
+ metadata_values = arrays["metadata_json"]
105
+ updated_metadata = []
106
+ for raw in metadata_values:
107
+ metadata = _json_loads(str(raw))
108
+ task_counts[str(metadata.get("task_id", "unknown"))] += 1
109
+ counters["rows"] += 1
110
+ source_dataset = str(metadata.get("source_dataset", ""))
111
+ observation_ref = str(metadata.get("observation_ref", ""))
112
+ if not source_dataset or not observation_ref:
113
+ counters["missing_observation_ref"] += 1
114
+ updated_metadata.append(json.dumps(metadata, sort_keys=True))
115
+ continue
116
+ key = (source_dataset, observation_ref)
117
+ if key not in ref_to_row:
118
+ ref_to_row[key] = len(embeddings)
119
+ embeddings.append(_embedding_for_ref(key, h5_cache))
120
+ metadata["observation_embedding_path"] = (
121
+ f"{embed_path.name}#embeddings/{ref_to_row[key]}"
122
+ )
123
+ metadata["observation_embedding_extractor"] = "rgb_jpeg_stats_v1"
124
+ metadata["observation_embedding_dim"] = OBSERVATION_EMBED_DIM
125
+ counters["rows_with_embedding"] += 1
126
+ updated_metadata.append(json.dumps(metadata, sort_keys=True))
127
+ arrays["metadata_json"] = np.asarray(updated_metadata)
128
+ np.savez_compressed(shard_path, **arrays)
129
+ updated_shards.append(str(shard_path))
130
+ finally:
131
+ for handle in h5_cache.values():
132
+ handle.close()
133
+
134
+ embedding_matrix = (
135
+ np.stack(embeddings).astype(np.float32)
136
+ if embeddings
137
+ else np.zeros((0, OBSERVATION_EMBED_DIM), dtype=np.float32)
138
+ )
139
+ np.savez_compressed(
140
+ embed_path,
141
+ embeddings=embedding_matrix,
142
+ extractor=np.asarray(["rgb_jpeg_stats_v1"]),
143
+ observation_refs=np.asarray([ref for _, ref in ref_to_row]),
144
+ source_datasets=np.asarray([source for source, _ in ref_to_row]),
145
+ )
146
+
147
+ index["observation_embedding_manifest"] = {
148
+ "path": embed_path.name,
149
+ "dataset": "embeddings",
150
+ "dim": OBSERVATION_EMBED_DIM,
151
+ "extractor": "rgb_jpeg_stats_v1",
152
+ "num_embeddings": int(embedding_matrix.shape[0]),
153
+ "reads_outcomes": False,
154
+ }
155
+ index["shard_content_hashes"] = {
156
+ str(Path(path).name): _sha256(Path(path)) for path in updated_shards
157
+ }
158
+ index["embedding_content_hash"] = _sha256(embed_path)
159
+ index["content_hash"] = _content_hash(index)
160
+ index_path.write_text(json.dumps(index, indent=2, sort_keys=True) + "\n")
161
+
162
+ return {
163
+ "split": split,
164
+ "index": str(index_path),
165
+ "rows": int(counters["rows"]),
166
+ "rows_with_embedding": int(counters["rows_with_embedding"]),
167
+ "missing_observation_ref": int(counters["missing_observation_ref"]),
168
+ "unique_observation_refs": int(embedding_matrix.shape[0]),
169
+ "embedding_path": str(embed_path),
170
+ "embedding_content_hash": index["embedding_content_hash"],
171
+ "content_hash_after": index["content_hash"],
172
+ "split_hash": index.get("split_hash"),
173
+ "task_counts": dict(sorted(task_counts.items())),
174
+ }
175
+
176
+
177
+ def _embedding_for_ref(key: tuple[str, str], h5_cache: dict[Path, Any]) -> np.ndarray:
178
+ source_dataset, observation_ref = key
179
+ archive_name, dataset_name, row_index = _parse_observation_ref(observation_ref)
180
+ archive_path = Path(source_dataset) / archive_name
181
+ if archive_path not in h5_cache:
182
+ try:
183
+ import h5py
184
+ except ImportError as exc: # pragma: no cover
185
+ raise ImportError("export_chart_observation_embeddings.py requires h5py") from exc
186
+ h5_cache[archive_path] = h5py.File(archive_path, "r")
187
+ payload = np.asarray(h5_cache[archive_path][dataset_name][row_index], dtype=np.uint8)
188
+ return _rgb_stats_embedding(payload.tobytes())
189
+
190
+
191
+ def _rgb_stats_embedding(jpeg_bytes: bytes) -> np.ndarray:
192
+ try:
193
+ from PIL import Image
194
+ except ImportError as exc: # pragma: no cover
195
+ raise ImportError("export_chart_observation_embeddings.py requires Pillow") from exc
196
+ image = Image.open(io.BytesIO(jpeg_bytes)).convert("RGB")
197
+ arr = np.asarray(image, dtype=np.float32) / 255.0
198
+ h, w, _ = arr.shape
199
+ y0, y1 = h // 4, h - h // 4
200
+ x0, x1 = w // 4, w - w // 4
201
+ center = arr[y0:y1, x0:x1]
202
+ grid_means = []
203
+ for y_slice in (slice(0, h // 2), slice(h // 2, h)):
204
+ for x_slice in (slice(0, w // 2), slice(w // 2, w)):
205
+ grid_means.extend(arr[y_slice, x_slice].mean(axis=(0, 1)).tolist())
206
+ luminance = arr.mean(axis=2)
207
+ hist, _ = np.histogram(luminance, bins=8, range=(0.0, 1.0), density=False)
208
+ hist = hist.astype(np.float32)
209
+ hist = hist / max(float(hist.sum()), 1.0)
210
+ feature = np.asarray(
211
+ [
212
+ *arr.mean(axis=(0, 1)).tolist(),
213
+ *arr.std(axis=(0, 1)).tolist(),
214
+ *center.mean(axis=(0, 1)).tolist(),
215
+ *center.std(axis=(0, 1)).tolist(),
216
+ *grid_means,
217
+ *hist.tolist(),
218
+ ],
219
+ dtype=np.float32,
220
+ )
221
+ if feature.shape[0] != OBSERVATION_EMBED_DIM:
222
+ raise AssertionError(f"expected {OBSERVATION_EMBED_DIM} dims, got {feature.shape[0]}")
223
+ return feature
224
+
225
+
226
+ def _parse_observation_ref(value: str) -> tuple[str, str, int]:
227
+ if "#" not in value:
228
+ raise ValueError(f"invalid observation_ref: {value}")
229
+ archive_name, ref = value.split("#", 1)
230
+ parts = [part for part in ref.split("/") if part]
231
+ if len(parts) != 2:
232
+ raise ValueError(f"invalid observation_ref: {value}")
233
+ return archive_name, parts[0], int(parts[1])
234
+
235
+
236
+ def _json_loads(value: str) -> dict[str, Any]:
237
+ try:
238
+ payload = json.loads(value)
239
+ except json.JSONDecodeError:
240
+ return {}
241
+ return payload if isinstance(payload, dict) else {}
242
+
243
+
244
+ def _metrics_by_task(rows: list[dict[str, Any]]) -> str:
245
+ payload: dict[str, dict[str, int]] = {}
246
+ for row in rows:
247
+ for task, count in row["task_counts"].items():
248
+ payload.setdefault(task, {})[row["split"]] = int(count)
249
+ return json.dumps(payload, indent=2, sort_keys=True)
250
+
251
+
252
+ def _table(rows: list[dict[str, Any]]) -> str:
253
+ lines = [
254
+ "% Auto-generated by scripts/export_chart_observation_embeddings.py",
255
+ "\\begin{tabular}{lrrr}",
256
+ "\\toprule",
257
+ "Split & Rows & With embedding & Unique refs \\\\",
258
+ "\\midrule",
259
+ ]
260
+ for row in rows:
261
+ lines.append(
262
+ f"{row['split']} & {row['rows']} & "
263
+ f"{row['rows_with_embedding']} & {row['unique_observation_refs']} \\\\"
264
+ )
265
+ lines.extend(["\\bottomrule", "\\end{tabular}"])
266
+ return "\n".join(lines)
267
+
268
+
269
+ def _report(metrics: dict[str, Any]) -> str:
270
+ lines = [
271
+ "# Chart Observation Embedding Export",
272
+ "",
273
+ "Decoded deployment-visible RGB observation refs into 32D statistics embeddings. "
274
+ "No outcome, label, or hidden-branch fields are read.",
275
+ "",
276
+ "| Split | Rows | With embedding | Missing refs | Unique refs |",
277
+ "| --- | ---: | ---: | ---: | ---: |",
278
+ ]
279
+ for row in metrics["splits"]:
280
+ lines.append(
281
+ f"| {row['split']} | {row['rows']} | {row['rows_with_embedding']} | "
282
+ f"{row['missing_observation_ref']} | {row['unique_observation_refs']} |"
283
+ )
284
+ return "\n".join(lines)
285
+
286
+
287
+ def _write_provenance(out_dir: Path, args: argparse.Namespace) -> None:
288
+ (out_dir / "config.yaml").write_text(
289
+ "\n".join(f"{key}: {value}" for key, value in sorted(vars(args).items())) + "\n"
290
+ )
291
+ (out_dir / "command.txt").write_text(
292
+ "python scripts/export_chart_observation_embeddings.py " + " ".join(sys.argv[1:]) + "\n"
293
+ )
294
+ (out_dir / "git_hash.txt").write_text(_run(["git", "rev-parse", "HEAD"]) + "\n")
295
+ hashes = {}
296
+ for index_path in args.indexes:
297
+ if index_path.exists():
298
+ index = json.loads(index_path.read_text())
299
+ hashes[str(index_path)] = {
300
+ "content_hash": index.get("content_hash"),
301
+ "split_hash": index.get("split_hash"),
302
+ }
303
+ (out_dir / "data_hash.txt").write_text(json.dumps(hashes, indent=2, sort_keys=True) + "\n")
304
+ (out_dir / "split_hash.txt").write_text(json.dumps(hashes, indent=2, sort_keys=True) + "\n")
305
+
306
+
307
+ def _sha256(path: Path) -> str:
308
+ digest = hashlib.sha256()
309
+ with path.open("rb") as handle:
310
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
311
+ digest.update(chunk)
312
+ return digest.hexdigest()
313
+
314
+
315
+ def _content_hash(index: dict[str, Any]) -> str:
316
+ payload = dict(index)
317
+ payload.pop("content_hash", None)
318
+ return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
319
+
320
+
321
+ def _run(command: list[str]) -> str:
322
+ try:
323
+ return subprocess.check_output(command, cwd=PROJECT_ROOT, text=True).strip()
324
+ except (subprocess.CalledProcessError, FileNotFoundError):
325
+ return ""
326
+
327
+
328
+ if __name__ == "__main__":
329
+ raise SystemExit(main())
workspace/scripts/slurm/train_ctt_feature_proxy.sbatch ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ #SBATCH --job-name=ctt_feature_proxy
3
+ #SBATCH --account=def-yalda
4
+ #SBATCH --nodes=1
5
+ #SBATCH --ntasks=1
6
+ #SBATCH --cpus-per-task=4
7
+ #SBATCH --mem=16G
8
+ #SBATCH --time=04:00:00
9
+ #SBATCH --array=0-2
10
+ #SBATCH --output=outputs/hpc/logs/%x_%A_%a.out
11
+ #SBATCH --error=outputs/hpc/logs/%x_%A_%a.err
12
+
13
+ set -euo pipefail
14
+
15
+ PROJECT_DIR="${PROJECT_DIR:-$SLURM_SUBMIT_DIR}"
16
+ cd "$PROJECT_DIR"
17
+ mkdir -p outputs/hpc/logs
18
+
19
+ export OMP_NUM_THREADS=1
20
+ export OPENBLAS_NUM_THREADS=1
21
+ export MKL_NUM_THREADS=1
22
+ export DOVLA_TORCH_THREADS=1
23
+ export PYTHONDONTWRITEBYTECODE=1
24
+
25
+ PYTHON="${PYTHON:-$PROJECT_DIR/.venv/bin/python}"
26
+ SEED="${SEED:-${SLURM_ARRAY_TASK_ID:-0}}"
27
+ VARIANT="${VARIANT:-residual}"
28
+ FEATURE_MODE="${FEATURE_MODE:-base}"
29
+ DATASET_INDEX="${DATASET_INDEX:-data/cil_charts/train/index.json}"
30
+ TARGET_INDEX="${TARGET_INDEX:-data/cil_charts/val/index.json}"
31
+ NAME_PREFIX="${NAME_PREFIX:-ctt_${VARIANT}_${FEATURE_MODE}}"
32
+ TRAIN_OUT="runs/${NAME_PREFIX}_seed${SEED}"
33
+ VAL_OUT="runs/${NAME_PREFIX}_seed${SEED}_val_proxy"
34
+
35
+ "$PYTHON" scripts/train_ctt.py \
36
+ --dataset "$DATASET_INDEX" \
37
+ --out-dir "$TRAIN_OUT" \
38
+ --variant "$VARIANT" \
39
+ --epochs "${CTT_EPOCHS:-5}" \
40
+ --max-charts "${CTT_MAX_CHARTS:-100000}" \
41
+ --neighbors "${CTT_NEIGHBORS:-8}" \
42
+ --lr "${CTT_LR:-0.001}" \
43
+ --seed "$SEED" \
44
+ --chart-feature-mode "$FEATURE_MODE" \
45
+ --pos-alignment "${CTT_POS_ALIGNMENT:-1.0}" \
46
+ --negative-boundary "${CTT_NEGATIVE_BOUNDARY:-0.25}" \
47
+ --pairwise-rank "${CTT_PAIRWISE_RANK:-1.0}" \
48
+ --listwise-rank "${CTT_LISTWISE_RANK:-0.5}" \
49
+ --cycle "${CTT_CYCLE:-0.1}" \
50
+ --diversity "${CTT_DIVERSITY:-0.05}" \
51
+ --negative-margin "${CTT_NEGATIVE_MARGIN:-0.2}" \
52
+ --transport-samples-per-pair "${CTT_TRANSPORT_SAMPLES_PER_PAIR:-4}" \
53
+ --diversity-temperature "${CTT_DIVERSITY_TEMPERATURE:-1.0}"
54
+
55
+ "$PYTHON" scripts/eval_ctt_proxy.py \
56
+ --checkpoint "$TRAIN_OUT/model.pt" \
57
+ --source-index "$DATASET_INDEX" \
58
+ --target-index "$TARGET_INDEX" \
59
+ --out-dir "$VAL_OUT" \
60
+ --k "${CTT_PROXY_K:-16}" \
61
+ --max-target-charts "${CTT_MAX_TARGET_CHARTS:-69}" \
62
+ --neighbors "${CTT_PROXY_NEIGHBORS:-8}" \
63
+ --thresholds "${CTT_PROXY_THRESHOLDS:-0.20,0.40}"
64
+
65
+ "$PYTHON" scripts/build_ctt_proxy_comparison.py \
66
+ --out-dir runs/ctt_val_proxy_comparison
67
+
68
+ "$PYTHON" scripts/summarize_ctt_runs.py \
69
+ --run-root runs \
70
+ --out-csv runs/summary_ctt.csv \
71
+ --out-md runs/summary_ctt.md