anhtld commited on
Commit
188c4d9
·
verified ·
1 Parent(s): 92b3b28

auto-sync 2026-07-03T17:25:27Z workspace (part 10)

Browse files
workspace/scripts/audit_chart_feature_sources.py CHANGED
@@ -128,6 +128,8 @@ def main(argv: list[str] | None = None) -> int:
128
  "schema_version": 1,
129
  "indexes": [str(path) for path in args.indexes],
130
  "splits": split_rows,
 
 
131
  "sample_details": row_details,
132
  "conclusion": _conclusion(split_rows),
133
  }
@@ -154,6 +156,13 @@ def _rate(count: int, total: int) -> float:
154
  return float(count) / float(total) if total else 0.0
155
 
156
 
 
 
 
 
 
 
 
157
  def _conclusion(rows: list[dict[str, Any]]) -> str:
158
  if all(float(row["observation_embedding_path_rate"]) > 0.0 for row in rows):
159
  if all(float(row["object_embedding_path_rate"]) > 0.0 for row in rows):
 
128
  "schema_version": 1,
129
  "indexes": [str(path) for path in args.indexes],
130
  "splits": split_rows,
131
+ "data_hash": {row["split"]: _index_hash(row, "content_hash") for row in split_rows},
132
+ "split_hash": {row["split"]: _index_hash(row, "split_hash") for row in split_rows},
133
  "sample_details": row_details,
134
  "conclusion": _conclusion(split_rows),
135
  }
 
156
  return float(count) / float(total) if total else 0.0
157
 
158
 
159
+ def _index_hash(row: dict[str, Any], key: str) -> Any:
160
+ path = Path(str(row["index"]))
161
+ if not path.exists():
162
+ return None
163
+ return json.loads(path.read_text()).get(key)
164
+
165
+
166
  def _conclusion(rows: list[dict[str, Any]]) -> str:
167
  if all(float(row["observation_embedding_path_rate"]) > 0.0 for row in rows):
168
  if all(float(row["object_embedding_path_rate"]) > 0.0 for row in rows):
workspace/scripts/build_ctt_rollout_comparison.py CHANGED
@@ -15,6 +15,12 @@ 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
  from scripts.eval_metrics import main as eval_metrics_main # noqa: E402
19
 
20
 
@@ -59,6 +65,8 @@ def main(argv: list[str] | None = None) -> int:
59
  "run_dirs": [str(path) for path in run_dirs],
60
  "train_seeds": [_seed_from_path(path) for path in run_dirs],
61
  "num_rows": len(combined_rows),
 
 
62
  "source_content_hash": _first(payloads, "source_content_hash"),
63
  "target_content_hash": _first(payloads, "target_content_hash"),
64
  "target_split_hash": _first(payloads, "target_split_hash"),
@@ -153,6 +161,15 @@ def _report(combined: dict[str, Any], metrics: dict[str, Any]) -> str:
153
  f"| selected_utility_mean | {_fmt(success.get('selected_utility_mean'))} |",
154
  f"| proposal_oracle_utility_mean | {_fmt(success.get('proposal_oracle_utility_mean'))} |",
155
  f"| hidden_chart_oracle_utility_mean | {_fmt(success.get('hidden_chart_oracle_utility_mean'))} |",
 
 
 
 
 
 
 
 
 
156
  "",
157
  ]
158
  )
@@ -200,13 +217,52 @@ def _success_summary(rows: list[dict[str, Any]], *, k: int) -> dict[str, Any]:
200
  selected_success_gain = []
201
  proposal_oracle_success_gain = []
202
  restore_errors = []
 
 
 
 
 
 
 
 
 
203
  for row in rows:
204
  generated_utilities = [float(value) for value in row.get("generated_utilities", [])[:k]]
205
  generated_success = [bool(value) for value in row.get("candidate_success", [])[:k]]
 
206
  selected_index = int(row.get("selected_index", 0))
207
  selected_success_value: float | None = None
208
  proposal_oracle_success_value: float | None = None
209
  base_success_value: float | None = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
210
  if "base_success" in row:
211
  base_success_value = float(bool(row["base_success"]))
212
  base_success.append(base_success_value)
@@ -259,10 +315,25 @@ def _success_summary(rows: list[dict[str, Any]], *, k: int) -> dict[str, Any]:
259
  "selected_utility_mean": _mean(selected_utility),
260
  "proposal_oracle_utility_mean": _mean(oracle_utility),
261
  "hidden_chart_oracle_utility_mean": _mean(hidden_oracle_utility),
 
 
 
 
 
 
 
 
 
262
  "max_restore_error": max(restore_errors) if restore_errors else None,
263
  }
264
 
265
 
 
 
 
 
 
 
266
  def _mean(values: list[float]) -> float | None:
267
  clean = [float(value) for value in values if math.isfinite(float(value))]
268
  return sum(clean) / len(clean) if clean else None
 
15
  if str(PROJECT_ROOT) not in sys.path:
16
  sys.path.insert(0, str(PROJECT_ROOT))
17
 
18
+ from cil.metrics import ( # noqa: E402
19
+ any_unsafe,
20
+ outcome_safety_violation,
21
+ safety_label_coverage,
22
+ unsafe_rate,
23
+ )
24
  from scripts.eval_metrics import main as eval_metrics_main # noqa: E402
25
 
26
 
 
65
  "run_dirs": [str(path) for path in run_dirs],
66
  "train_seeds": [_seed_from_path(path) for path in run_dirs],
67
  "num_rows": len(combined_rows),
68
+ "data_hash": _first(payloads, "target_content_hash"),
69
+ "split_hash": _first(payloads, "target_split_hash"),
70
  "source_content_hash": _first(payloads, "source_content_hash"),
71
  "target_content_hash": _first(payloads, "target_content_hash"),
72
  "target_split_hash": _first(payloads, "target_split_hash"),
 
161
  f"| selected_utility_mean | {_fmt(success.get('selected_utility_mean'))} |",
162
  f"| proposal_oracle_utility_mean | {_fmt(success.get('proposal_oracle_utility_mean'))} |",
163
  f"| hidden_chart_oracle_utility_mean | {_fmt(success.get('hidden_chart_oracle_utility_mean'))} |",
164
+ f"| generated_safety_label_coverage | {_fmt(success.get('generated_safety_label_coverage'))} |",
165
+ f"| generated_unsafe_rate_known | {_fmt(success.get('generated_unsafe_rate_known'))} |",
166
+ f"| any_generated_unsafe_known | {_fmt(success.get('any_generated_unsafe_known'))} |",
167
+ f"| selected_safety_label_known_rate | {_fmt(success.get('selected_safety_label_known_rate'))} |",
168
+ f"| selected_unsafe_rate_known | {_fmt(success.get('selected_unsafe_rate_known'))} |",
169
+ f"| proposal_oracle_safety_label_known_rate | {_fmt(success.get('proposal_oracle_safety_label_known_rate'))} |",
170
+ f"| proposal_oracle_unsafe_rate_known | {_fmt(success.get('proposal_oracle_unsafe_rate_known'))} |",
171
+ f"| base_safety_label_known_rate | {_fmt(success.get('base_safety_label_known_rate'))} |",
172
+ f"| base_unsafe_rate_known | {_fmt(success.get('base_unsafe_rate_known'))} |",
173
  "",
174
  ]
175
  )
 
217
  selected_success_gain = []
218
  proposal_oracle_success_gain = []
219
  restore_errors = []
220
+ generated_safety_coverage = []
221
+ generated_unsafe = []
222
+ any_generated_unsafe = []
223
+ selected_safety_known = []
224
+ selected_unsafe = []
225
+ proposal_oracle_safety_known = []
226
+ proposal_oracle_unsafe = []
227
+ base_safety_known = []
228
+ base_unsafe = []
229
  for row in rows:
230
  generated_utilities = [float(value) for value in row.get("generated_utilities", [])[:k]]
231
  generated_success = [bool(value) for value in row.get("candidate_success", [])[:k]]
232
+ candidate_outcomes = _outcome_list(row.get("candidate_outcomes", []))[:k]
233
  selected_index = int(row.get("selected_index", 0))
234
  selected_success_value: float | None = None
235
  proposal_oracle_success_value: float | None = None
236
  base_success_value: float | None = None
237
+ base_outcome = row.get("base_outcome")
238
+ if isinstance(base_outcome, dict):
239
+ safety = outcome_safety_violation(base_outcome)
240
+ base_safety_known.append(float(safety is not None))
241
+ if safety is not None:
242
+ base_unsafe.append(float(safety))
243
+ if candidate_outcomes:
244
+ generated_safety_coverage.append(safety_label_coverage(candidate_outcomes, k=k))
245
+ unsafe = unsafe_rate(candidate_outcomes, k=k)
246
+ if unsafe is not None:
247
+ generated_unsafe.append(unsafe)
248
+ any_unsafe_value = any_unsafe(candidate_outcomes, k=k)
249
+ if any_unsafe_value is not None:
250
+ any_generated_unsafe.append(any_unsafe_value)
251
+ if selected_index < len(candidate_outcomes):
252
+ safety = outcome_safety_violation(candidate_outcomes[selected_index])
253
+ selected_safety_known.append(float(safety is not None))
254
+ if safety is not None:
255
+ selected_unsafe.append(float(safety))
256
+ if generated_utilities:
257
+ oracle_index = max(
258
+ range(len(generated_utilities)),
259
+ key=lambda index: generated_utilities[index],
260
+ )
261
+ if oracle_index < len(candidate_outcomes):
262
+ safety = outcome_safety_violation(candidate_outcomes[oracle_index])
263
+ proposal_oracle_safety_known.append(float(safety is not None))
264
+ if safety is not None:
265
+ proposal_oracle_unsafe.append(float(safety))
266
  if "base_success" in row:
267
  base_success_value = float(bool(row["base_success"]))
268
  base_success.append(base_success_value)
 
315
  "selected_utility_mean": _mean(selected_utility),
316
  "proposal_oracle_utility_mean": _mean(oracle_utility),
317
  "hidden_chart_oracle_utility_mean": _mean(hidden_oracle_utility),
318
+ "generated_safety_label_coverage": _mean(generated_safety_coverage),
319
+ "generated_unsafe_rate_known": _mean(generated_unsafe),
320
+ "any_generated_unsafe_known": _mean(any_generated_unsafe),
321
+ "selected_safety_label_known_rate": _mean(selected_safety_known),
322
+ "selected_unsafe_rate_known": _mean(selected_unsafe),
323
+ "proposal_oracle_safety_label_known_rate": _mean(proposal_oracle_safety_known),
324
+ "proposal_oracle_unsafe_rate_known": _mean(proposal_oracle_unsafe),
325
+ "base_safety_label_known_rate": _mean(base_safety_known),
326
+ "base_unsafe_rate_known": _mean(base_unsafe),
327
  "max_restore_error": max(restore_errors) if restore_errors else None,
328
  }
329
 
330
 
331
+ def _outcome_list(value: Any) -> list[dict[str, Any]]:
332
+ if not isinstance(value, list):
333
+ return []
334
+ return [item for item in value if isinstance(item, dict)]
335
+
336
+
337
  def _mean(values: list[float]) -> float | None:
338
  clean = [float(value) for value in values if math.isfinite(float(value))]
339
  return sum(clean) / len(clean) if clean else None
workspace/scripts/eval_dominance_selector.py CHANGED
@@ -147,6 +147,8 @@ def main(argv: list[str] | None = None) -> int:
147
  "residual_quantile": residual_quantile,
148
  "calibration_input": str(args.calibration_input),
149
  "eval_input": str(args.eval_input),
 
 
150
  "calibration_target_content_hash": calibration_index.get("content_hash"),
151
  "calibration_target_split_hash": calibration_index.get("split_hash"),
152
  "eval_target_content_hash": eval_index.get("content_hash"),
 
147
  "residual_quantile": residual_quantile,
148
  "calibration_input": str(args.calibration_input),
149
  "eval_input": str(args.eval_input),
150
+ "data_hash": eval_index.get("content_hash"),
151
+ "split_hash": eval_index.get("split_hash"),
152
  "calibration_target_content_hash": calibration_index.get("content_hash"),
153
  "calibration_target_split_hash": calibration_index.get("split_hash"),
154
  "eval_target_content_hash": eval_index.get("content_hash"),
workspace/scripts/eval_learned_dominance_selector.py CHANGED
@@ -157,6 +157,8 @@ def main(argv: list[str] | None = None) -> int:
157
  "feature_std": best["std"].tolist(),
158
  "calibration_input": str(args.calibration_input),
159
  "eval_input": str(args.eval_input),
 
 
160
  "calibration_target_content_hash": calibration_index.get("content_hash"),
161
  "calibration_target_split_hash": calibration_index.get("split_hash"),
162
  "eval_target_content_hash": eval_index.get("content_hash"),
 
157
  "feature_std": best["std"].tolist(),
158
  "calibration_input": str(args.calibration_input),
159
  "eval_input": str(args.eval_input),
160
+ "data_hash": eval_index.get("content_hash"),
161
+ "split_hash": eval_index.get("split_hash"),
162
  "calibration_target_content_hash": calibration_index.get("content_hash"),
163
  "calibration_target_split_hash": calibration_index.get("split_hash"),
164
  "eval_target_content_hash": eval_index.get("content_hash"),
workspace/scripts/eval_metrics.py CHANGED
@@ -15,6 +15,7 @@ if str(PROJECT_ROOT) not in sys.path:
15
 
16
  from cil.metrics import ( # noqa: E402
17
  MetricInputError,
 
18
  branch_car,
19
  candidate_diversity,
20
  collapse_rate,
@@ -28,6 +29,10 @@ from cil.metrics import ( # noqa: E402
28
  proxy_positive_tangent_coverage_at_k,
29
  proxy_support_distance,
30
  selector_regret_at_k,
 
 
 
 
31
  )
32
 
33
 
@@ -131,6 +136,7 @@ def _measured_row(row: dict[str, Any], *, k: int, epsilon: float) -> dict[str, A
131
  hidden = _numbers(row, "hidden_chart_utilities", required=False)
132
  candidate_success = _bool_numbers(row, "candidate_success", required=False)
133
  base_success = _optional_bool(row.get("base_success"))
 
134
  selected_utility = utilities[selected_index]
135
  prefix = utilities[:k]
136
  output = _base_row(row, mode="measured")
@@ -159,6 +165,46 @@ def _measured_row(row: dict[str, Any], *, k: int, epsilon: float) -> dict[str, A
159
  )
160
  if base_success is not None:
161
  output["base_success"] = float(base_success)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  if candidate_success:
163
  success_prefix = candidate_success[:k]
164
  selected_success = float(success_prefix[selected_index])
@@ -297,6 +343,22 @@ def _matrix(row: dict[str, Any], key: str, *, required: bool = True) -> list[lis
297
  return [[float(item) for item in vector] for vector in values]
298
 
299
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  def _parse_thresholds(raw: str) -> list[float]:
301
  values = [float(item.strip()) for item in raw.split(",") if item.strip()]
302
  if not values or any(value < 0.0 for value in values):
 
15
 
16
  from cil.metrics import ( # noqa: E402
17
  MetricInputError,
18
+ any_unsafe,
19
  branch_car,
20
  candidate_diversity,
21
  collapse_rate,
 
29
  proxy_positive_tangent_coverage_at_k,
30
  proxy_support_distance,
31
  selector_regret_at_k,
32
+ selected_unsafe,
33
+ safety_label_coverage,
34
+ outcome_safety_violation,
35
+ unsafe_rate,
36
  )
37
 
38
 
 
136
  hidden = _numbers(row, "hidden_chart_utilities", required=False)
137
  candidate_success = _bool_numbers(row, "candidate_success", required=False)
138
  base_success = _optional_bool(row.get("base_success"))
139
+ candidate_outcomes = _outcomes(row, "candidate_outcomes", required=False)
140
  selected_utility = utilities[selected_index]
141
  prefix = utilities[:k]
142
  output = _base_row(row, mode="measured")
 
165
  )
166
  if base_success is not None:
167
  output["base_success"] = float(base_success)
168
+ base_outcome = row.get("base_outcome")
169
+ if isinstance(base_outcome, dict):
170
+ base_safety = outcome_safety_violation(base_outcome)
171
+ output["base_safety_label_known"] = float(base_safety is not None)
172
+ if base_safety is not None:
173
+ output["base_unsafe_known"] = float(base_safety)
174
+ if candidate_outcomes:
175
+ output[f"generated_safety_label_coverage_at_{k}"] = safety_label_coverage(
176
+ candidate_outcomes,
177
+ k=k,
178
+ )
179
+ generated_unsafe = unsafe_rate(candidate_outcomes, k=k)
180
+ if generated_unsafe is not None:
181
+ output[f"generated_unsafe_rate_known_at_{k}"] = generated_unsafe
182
+ any_generated_unsafe = any_unsafe(candidate_outcomes, k=k)
183
+ if any_generated_unsafe is not None:
184
+ output[f"any_generated_unsafe_known_at_{k}"] = any_generated_unsafe
185
+ if selected_index < min(k, len(candidate_outcomes)):
186
+ selected_safety = outcome_safety_violation(candidate_outcomes[selected_index])
187
+ output[f"selected_safety_label_known_at_{k}"] = float(
188
+ selected_safety is not None
189
+ )
190
+ selected_safety_value = selected_unsafe(
191
+ candidate_outcomes,
192
+ selected_index=selected_index,
193
+ k=k,
194
+ )
195
+ if selected_safety_value is not None:
196
+ output[f"selected_unsafe_known_at_{k}"] = selected_safety_value
197
+ if prefix:
198
+ oracle_index = max(range(len(prefix)), key=lambda item: prefix[item])
199
+ if oracle_index < len(candidate_outcomes):
200
+ oracle_safety = outcome_safety_violation(candidate_outcomes[oracle_index])
201
+ output[f"proposal_oracle_safety_label_known_at_{k}"] = float(
202
+ oracle_safety is not None
203
+ )
204
+ if oracle_safety is not None:
205
+ output[f"proposal_oracle_unsafe_known_at_{k}"] = float(
206
+ oracle_safety
207
+ )
208
  if candidate_success:
209
  success_prefix = candidate_success[:k]
210
  selected_success = float(success_prefix[selected_index])
 
343
  return [[float(item) for item in vector] for vector in values]
344
 
345
 
346
+ def _outcomes(row: dict[str, Any], key: str, *, required: bool = True) -> list[dict[str, Any]]:
347
+ values = row.get(key)
348
+ if values is None:
349
+ if required:
350
+ raise MetricInputError(f"row requires {key}")
351
+ return []
352
+ if not isinstance(values, list):
353
+ raise MetricInputError(f"{key} must be a list of outcome objects")
354
+ outcomes: list[dict[str, Any]] = []
355
+ for index, value in enumerate(values):
356
+ if not isinstance(value, dict):
357
+ raise MetricInputError(f"{key}[{index}] must be an outcome object")
358
+ outcomes.append(value)
359
+ return outcomes
360
+
361
+
362
  def _parse_thresholds(raw: str) -> list[float]:
363
  values = [float(item.strip()) for item in raw.split(",") if item.strip()]
364
  if not values or any(value < 0.0 for value in values):
workspace/scripts/eval_nonlinear_dominance_selector.py CHANGED
@@ -182,6 +182,8 @@ def main(argv: list[str] | None = None) -> int:
182
  "chart_feature_mode": chart_feature_mode,
183
  "calibration_input": str(args.calibration_input),
184
  "eval_input": str(args.eval_input),
 
 
185
  "calibration_target_content_hash": calibration_index.get("content_hash"),
186
  "calibration_target_split_hash": calibration_index.get("split_hash"),
187
  "eval_target_content_hash": eval_index.get("content_hash"),
 
182
  "chart_feature_mode": chart_feature_mode,
183
  "calibration_input": str(args.calibration_input),
184
  "eval_input": str(args.eval_input),
185
+ "data_hash": eval_index.get("content_hash"),
186
+ "split_hash": eval_index.get("split_hash"),
187
  "calibration_target_content_hash": calibration_index.get("content_hash"),
188
  "calibration_target_split_hash": calibration_index.get("split_hash"),
189
  "eval_target_content_hash": eval_index.get("content_hash"),
workspace/scripts/export_chart_object_embeddings.py ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 OBJECT_LAYOUT_EMBED_DIM # noqa: E402
21
+
22
+
23
+ EXTRACTOR_NAME = "rgb_object_layout_v1"
24
+
25
+
26
+ def main(argv: list[str] | None = None) -> int:
27
+ parser = argparse.ArgumentParser(
28
+ description=(
29
+ "Decode deployment-visible observation_ref JPEGs and write a "
30
+ "deterministic RGB object-layout embedding into chart metadata."
31
+ )
32
+ )
33
+ parser.add_argument(
34
+ "--indexes",
35
+ nargs="+",
36
+ type=Path,
37
+ default=[
38
+ Path("data/cil_charts_rgb_refs/train/index.json"),
39
+ Path("data/cil_charts_rgb_refs/val/index.json"),
40
+ Path("data/cil_charts_rgb_refs/test/index.json"),
41
+ ],
42
+ )
43
+ parser.add_argument(
44
+ "--out-dir",
45
+ type=Path,
46
+ default=Path("runs/chart_object_embeddings_rgb_refs"),
47
+ )
48
+ parser.add_argument("--overwrite", action="store_true")
49
+ args = parser.parse_args(argv)
50
+
51
+ out_dir = args.out_dir
52
+ out_dir.mkdir(parents=True, exist_ok=True)
53
+ _write_provenance(out_dir, args)
54
+
55
+ split_rows = []
56
+ for index_path in args.indexes:
57
+ split_rows.append(_process_index(index_path, overwrite=args.overwrite))
58
+
59
+ metrics = {
60
+ "report_type": "chart_object_embedding_export",
61
+ "schema_version": 1,
62
+ "embedding_dim": OBJECT_LAYOUT_EMBED_DIM,
63
+ "extractor": EXTRACTOR_NAME,
64
+ "indexes": [str(path) for path in args.indexes],
65
+ "splits": split_rows,
66
+ "data_hash": {row["split"]: row["content_hash_after"] for row in split_rows},
67
+ "split_hash": {row["split"]: row["split_hash"] for row in split_rows},
68
+ "leakage_contract": {
69
+ "reads_outcomes": False,
70
+ "reads_observation_ref": True,
71
+ "writes_object_embedding_path": True,
72
+ },
73
+ }
74
+ (out_dir / "metrics.json").write_text(json.dumps(metrics, indent=2, sort_keys=True) + "\n")
75
+ (out_dir / "metrics_by_task.json").write_text(_metrics_by_task(split_rows) + "\n")
76
+ (out_dir / "metrics_by_seed.json").write_text("{}\n")
77
+ (out_dir / "table.tex").write_text(_table(split_rows) + "\n")
78
+ (out_dir / "report.md").write_text(_report(metrics) + "\n")
79
+ (out_dir / "train.log").write_text("not a training run; exported object-layout embeddings\n")
80
+ (out_dir / "eval.log").write_text(
81
+ "decoded observation_ref JPEGs only; no outcomes, labels, or hidden branches read\n"
82
+ )
83
+ print(json.dumps({"out_dir": str(out_dir), "splits": len(split_rows)}, indent=2))
84
+ return 0
85
+
86
+
87
+ def _process_index(index_path: Path, *, overwrite: bool) -> dict[str, Any]:
88
+ index = json.loads(index_path.read_text())
89
+ split = str(index.get("split", index_path.parent.name))
90
+ embed_path = index_path.parent / "object_embeddings_rgb_layout.npz"
91
+ if embed_path.exists() and not overwrite:
92
+ raise FileExistsError(f"{embed_path} exists; pass --overwrite to replace it")
93
+
94
+ ref_to_row: dict[tuple[str, str], int] = {}
95
+ embeddings: list[np.ndarray] = []
96
+ counters: Counter[str] = Counter()
97
+ task_counts: Counter[str] = Counter()
98
+ updated_shards: list[str] = []
99
+ h5_cache: dict[Path, Any] = {}
100
+ try:
101
+ for shard in index.get("shards", []):
102
+ shard_path = index_path.parent / str(shard["path"])
103
+ with np.load(shard_path, allow_pickle=False) as data:
104
+ arrays = {key: data[key] for key in data.files}
105
+ metadata_values = arrays["metadata_json"]
106
+ updated_metadata = []
107
+ for raw in metadata_values:
108
+ metadata = _json_loads(str(raw))
109
+ task_counts[str(metadata.get("task_id", "unknown"))] += 1
110
+ counters["rows"] += 1
111
+ source_dataset = str(metadata.get("source_dataset", ""))
112
+ observation_ref = str(metadata.get("observation_ref", ""))
113
+ if not source_dataset or not observation_ref:
114
+ counters["missing_observation_ref"] += 1
115
+ updated_metadata.append(json.dumps(metadata, sort_keys=True))
116
+ continue
117
+ key = (source_dataset, observation_ref)
118
+ if key not in ref_to_row:
119
+ ref_to_row[key] = len(embeddings)
120
+ embeddings.append(_embedding_for_ref(key, h5_cache))
121
+ metadata["object_embedding_path"] = (
122
+ f"{embed_path.name}#embeddings/{ref_to_row[key]}"
123
+ )
124
+ metadata["object_embedding_extractor"] = EXTRACTOR_NAME
125
+ metadata["object_embedding_dim"] = OBJECT_LAYOUT_EMBED_DIM
126
+ counters["rows_with_embedding"] += 1
127
+ updated_metadata.append(json.dumps(metadata, sort_keys=True))
128
+ arrays["metadata_json"] = np.asarray(updated_metadata)
129
+ np.savez_compressed(shard_path, **arrays)
130
+ updated_shards.append(str(shard_path))
131
+ finally:
132
+ for handle in h5_cache.values():
133
+ handle.close()
134
+
135
+ embedding_matrix = (
136
+ np.stack(embeddings).astype(np.float32)
137
+ if embeddings
138
+ else np.zeros((0, OBJECT_LAYOUT_EMBED_DIM), dtype=np.float32)
139
+ )
140
+ np.savez_compressed(
141
+ embed_path,
142
+ embeddings=embedding_matrix,
143
+ extractor=np.asarray([EXTRACTOR_NAME]),
144
+ observation_refs=np.asarray([ref for _, ref in ref_to_row]),
145
+ source_datasets=np.asarray([source for source, _ in ref_to_row]),
146
+ )
147
+
148
+ index["object_embedding_manifest"] = {
149
+ "path": embed_path.name,
150
+ "dataset": "embeddings",
151
+ "dim": OBJECT_LAYOUT_EMBED_DIM,
152
+ "extractor": EXTRACTOR_NAME,
153
+ "num_embeddings": int(embedding_matrix.shape[0]),
154
+ "reads_outcomes": False,
155
+ }
156
+ index["shard_content_hashes"] = {
157
+ str(Path(path).name): _sha256(Path(path)) for path in updated_shards
158
+ }
159
+ index["object_embedding_content_hash"] = _sha256(embed_path)
160
+ index["content_hash"] = _content_hash(index)
161
+ index_path.write_text(json.dumps(index, indent=2, sort_keys=True) + "\n")
162
+
163
+ return {
164
+ "split": split,
165
+ "index": str(index_path),
166
+ "rows": int(counters["rows"]),
167
+ "rows_with_embedding": int(counters["rows_with_embedding"]),
168
+ "missing_observation_ref": int(counters["missing_observation_ref"]),
169
+ "unique_observation_refs": int(embedding_matrix.shape[0]),
170
+ "embedding_path": str(embed_path),
171
+ "embedding_content_hash": index["object_embedding_content_hash"],
172
+ "content_hash_after": index["content_hash"],
173
+ "split_hash": index.get("split_hash"),
174
+ "task_counts": dict(sorted(task_counts.items())),
175
+ }
176
+
177
+
178
+ def _embedding_for_ref(key: tuple[str, str], h5_cache: dict[Path, Any]) -> np.ndarray:
179
+ source_dataset, observation_ref = key
180
+ archive_name, dataset_name, row_index = _parse_observation_ref(observation_ref)
181
+ archive_path = Path(source_dataset) / archive_name
182
+ if archive_path not in h5_cache:
183
+ try:
184
+ import h5py
185
+ except ImportError as exc: # pragma: no cover
186
+ raise ImportError("export_chart_object_embeddings.py requires h5py") from exc
187
+ h5_cache[archive_path] = h5py.File(archive_path, "r")
188
+ payload = np.asarray(h5_cache[archive_path][dataset_name][row_index], dtype=np.uint8)
189
+ return _object_layout_embedding(payload.tobytes())
190
+
191
+
192
+ def _object_layout_embedding(jpeg_bytes: bytes) -> np.ndarray:
193
+ try:
194
+ from PIL import Image
195
+ except ImportError as exc: # pragma: no cover
196
+ raise ImportError("export_chart_object_embeddings.py requires Pillow") from exc
197
+
198
+ image = Image.open(io.BytesIO(jpeg_bytes)).convert("RGB").resize((96, 96))
199
+ arr = np.asarray(image, dtype=np.float32) / 255.0
200
+ gray = arr.mean(axis=2)
201
+ saturation = arr.max(axis=2) - arr.min(axis=2)
202
+ gy = np.zeros_like(gray)
203
+ gx = np.zeros_like(gray)
204
+ gy[1:, :] = np.abs(gray[1:, :] - gray[:-1, :])
205
+ gx[:, 1:] = np.abs(gray[:, 1:] - gray[:, :-1])
206
+ edge = gx + gy
207
+ score = saturation + 0.5 * edge + 0.5 * np.abs(gray - float(np.median(gray)))
208
+ threshold = max(float(np.quantile(score, 0.75)), float(score.mean() + 0.25 * score.std()))
209
+ mask = score >= threshold
210
+ components = _connected_components(mask)
211
+ components = [component for component in components if len(component[0]) >= 8]
212
+ components.sort(key=lambda component: len(component[0]), reverse=True)
213
+
214
+ features: list[float] = []
215
+ for ys, xs in components[:4]:
216
+ features.extend(_component_features(arr, gray, saturation, edge, ys, xs))
217
+ while len(features) < OBJECT_LAYOUT_EMBED_DIM:
218
+ features.append(0.0)
219
+ output = np.asarray(features[:OBJECT_LAYOUT_EMBED_DIM], dtype=np.float32)
220
+ if output.shape[0] != OBJECT_LAYOUT_EMBED_DIM:
221
+ raise AssertionError(f"expected {OBJECT_LAYOUT_EMBED_DIM} dims, got {output.shape[0]}")
222
+ return output
223
+
224
+
225
+ def _connected_components(mask: np.ndarray) -> list[tuple[np.ndarray, np.ndarray]]:
226
+ height, width = mask.shape
227
+ visited = np.zeros_like(mask, dtype=bool)
228
+ components: list[tuple[np.ndarray, np.ndarray]] = []
229
+ for start_y in range(height):
230
+ for start_x in range(width):
231
+ if not mask[start_y, start_x] or visited[start_y, start_x]:
232
+ continue
233
+ stack = [(start_y, start_x)]
234
+ visited[start_y, start_x] = True
235
+ ys: list[int] = []
236
+ xs: list[int] = []
237
+ while stack:
238
+ y, x = stack.pop()
239
+ ys.append(y)
240
+ xs.append(x)
241
+ for next_y, next_x in (
242
+ (y - 1, x),
243
+ (y + 1, x),
244
+ (y, x - 1),
245
+ (y, x + 1),
246
+ ):
247
+ if (
248
+ 0 <= next_y < height
249
+ and 0 <= next_x < width
250
+ and mask[next_y, next_x]
251
+ and not visited[next_y, next_x]
252
+ ):
253
+ visited[next_y, next_x] = True
254
+ stack.append((next_y, next_x))
255
+ components.append((np.asarray(ys, dtype=np.int32), np.asarray(xs, dtype=np.int32)))
256
+ return components
257
+
258
+
259
+ def _component_features(
260
+ arr: np.ndarray,
261
+ gray: np.ndarray,
262
+ saturation: np.ndarray,
263
+ edge: np.ndarray,
264
+ ys: np.ndarray,
265
+ xs: np.ndarray,
266
+ ) -> list[float]:
267
+ height, width, _ = arr.shape
268
+ pixels = arr[ys, xs]
269
+ y_norm = ys.astype(np.float32) / max(float(height - 1), 1.0)
270
+ x_norm = xs.astype(np.float32) / max(float(width - 1), 1.0)
271
+ bbox_w = (float(xs.max() - xs.min() + 1) / float(width)) if xs.size else 0.0
272
+ bbox_h = (float(ys.max() - ys.min() + 1) / float(height)) if ys.size else 0.0
273
+ return [
274
+ float(xs.size) / float(height * width),
275
+ float(2.0 * x_norm.mean() - 1.0),
276
+ float(2.0 * y_norm.mean() - 1.0),
277
+ float(2.0 * x_norm.std()),
278
+ float(2.0 * y_norm.std()),
279
+ bbox_w,
280
+ bbox_h,
281
+ *pixels.mean(axis=0).astype(float).tolist(),
282
+ *pixels.std(axis=0).astype(float).tolist(),
283
+ float(gray[ys, xs].mean()),
284
+ float(saturation[ys, xs].mean()),
285
+ float(edge[ys, xs].mean()),
286
+ ]
287
+
288
+
289
+ def _parse_observation_ref(value: str) -> tuple[str, str, int]:
290
+ if "#" not in value:
291
+ raise ValueError(f"invalid observation_ref: {value}")
292
+ archive_name, ref = value.split("#", 1)
293
+ parts = [part for part in ref.split("/") if part]
294
+ if len(parts) != 2:
295
+ raise ValueError(f"invalid observation_ref: {value}")
296
+ return archive_name, parts[0], int(parts[1])
297
+
298
+
299
+ def _json_loads(value: str) -> dict[str, Any]:
300
+ try:
301
+ payload = json.loads(value)
302
+ except json.JSONDecodeError:
303
+ return {}
304
+ return payload if isinstance(payload, dict) else {}
305
+
306
+
307
+ def _metrics_by_task(rows: list[dict[str, Any]]) -> str:
308
+ payload: dict[str, dict[str, int]] = {}
309
+ for row in rows:
310
+ for task, count in row["task_counts"].items():
311
+ payload.setdefault(task, {})[row["split"]] = int(count)
312
+ return json.dumps(payload, indent=2, sort_keys=True)
313
+
314
+
315
+ def _table(rows: list[dict[str, Any]]) -> str:
316
+ lines = [
317
+ "% Auto-generated by scripts/export_chart_object_embeddings.py",
318
+ "\\begin{tabular}{lrrr}",
319
+ "\\toprule",
320
+ "Split & Rows & With object embed & Unique refs \\\\",
321
+ "\\midrule",
322
+ ]
323
+ for row in rows:
324
+ lines.append(
325
+ f"{row['split']} & {row['rows']} & "
326
+ f"{row['rows_with_embedding']} & {row['unique_observation_refs']} \\\\"
327
+ )
328
+ lines.extend(["\\bottomrule", "\\end{tabular}"])
329
+ return "\n".join(lines)
330
+
331
+
332
+ def _report(metrics: dict[str, Any]) -> str:
333
+ lines = [
334
+ "# Chart Object-Layout Embedding Export",
335
+ "",
336
+ "Decoded deployment-visible RGB observation refs into deterministic 64D "
337
+ "foreground component/layout embeddings. No outcome, label, or hidden-branch "
338
+ "fields are read.",
339
+ "",
340
+ "| Split | Rows | With embedding | Missing refs | Unique refs |",
341
+ "| --- | ---: | ---: | ---: | ---: |",
342
+ ]
343
+ for row in metrics["splits"]:
344
+ lines.append(
345
+ f"| {row['split']} | {row['rows']} | {row['rows_with_embedding']} | "
346
+ f"{row['missing_observation_ref']} | {row['unique_observation_refs']} |"
347
+ )
348
+ return "\n".join(lines)
349
+
350
+
351
+ def _write_provenance(out_dir: Path, args: argparse.Namespace) -> None:
352
+ (out_dir / "config.yaml").write_text(
353
+ "\n".join(f"{key}: {value}" for key, value in sorted(vars(args).items())) + "\n"
354
+ )
355
+ (out_dir / "command.txt").write_text(
356
+ "python scripts/export_chart_object_embeddings.py " + " ".join(sys.argv[1:]) + "\n"
357
+ )
358
+ (out_dir / "git_hash.txt").write_text(_run(["git", "rev-parse", "HEAD"]) + "\n")
359
+ hashes = {}
360
+ for index_path in args.indexes:
361
+ if index_path.exists():
362
+ index = json.loads(index_path.read_text())
363
+ hashes[str(index_path)] = {
364
+ "content_hash": index.get("content_hash"),
365
+ "split_hash": index.get("split_hash"),
366
+ }
367
+ (out_dir / "data_hash.txt").write_text(json.dumps(hashes, indent=2, sort_keys=True) + "\n")
368
+ (out_dir / "split_hash.txt").write_text(json.dumps(hashes, indent=2, sort_keys=True) + "\n")
369
+
370
+
371
+ def _sha256(path: Path) -> str:
372
+ digest = hashlib.sha256()
373
+ with path.open("rb") as handle:
374
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
375
+ digest.update(chunk)
376
+ return digest.hexdigest()
377
+
378
+
379
+ def _content_hash(index: dict[str, Any]) -> str:
380
+ payload = dict(index)
381
+ payload.pop("content_hash", None)
382
+ return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
383
+
384
+
385
+ def _run(command: list[str]) -> str:
386
+ try:
387
+ return subprocess.check_output(command, cwd=PROJECT_ROOT, text=True).strip()
388
+ except (subprocess.CalledProcessError, FileNotFoundError):
389
+ return ""
390
+
391
+
392
+ if __name__ == "__main__":
393
+ raise SystemExit(main())