anhtld commited on
Commit
648e418
·
verified ·
1 Parent(s): 9ab0f5a

auto-sync 2026-07-04T01:14:16Z workspace (part 3)

Browse files
workspace/scripts/eval_learned_dominance_selector.py CHANGED
@@ -18,8 +18,10 @@ if str(PROJECT_ROOT) not in sys.path:
18
 
19
  import numpy as np # noqa: E402
20
 
 
21
  from cil.metrics import macro_micro_summary # noqa: E402
22
  from scripts.eval_dominance_selector import _DominanceScorer, _chart_map, _first_train_seed, _rows # noqa: E402
 
23
 
24
 
25
  BASIC_FEATURE_NAMES = [
@@ -56,6 +58,30 @@ SOURCE_EVIDENCE_NAMES = [
56
  "generated_to_source_negative_min_rms",
57
  "generated_source_pos_closer_than_neg",
58
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  FEATURE_SET_CHOICES = (
60
  "basic",
61
  "tangent",
@@ -65,6 +91,10 @@ FEATURE_SET_CHOICES = (
65
  "tangent_source_evidence",
66
  "context_source_evidence",
67
  "context_tangent_source_evidence",
 
 
 
 
68
  )
69
 
70
 
@@ -101,6 +131,15 @@ def main(argv: list[str] | None = None) -> int:
101
  default="basic",
102
  help="Deployment-visible feature family for candidate-level dominance fitting.",
103
  )
 
 
 
 
 
 
 
 
 
104
  parser.add_argument(
105
  "--target",
106
  choices=("utility_margin", "success", "success_weighted_margin"),
@@ -166,6 +205,25 @@ def main(argv: list[str] | None = None) -> int:
166
  source_evidence, source_index = (
167
  _source_evidence_map(source_index_path) if _uses_source_evidence(args.feature_set) else ({}, {})
168
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
  calibration_dataset = _candidate_dataset(
171
  calibration_rows,
@@ -175,6 +233,9 @@ def main(argv: list[str] | None = None) -> int:
175
  feature_set=args.feature_set,
176
  target=args.target,
177
  source_evidence=source_evidence,
 
 
 
178
  )
179
  eval_dataset = _candidate_dataset(
180
  eval_rows,
@@ -184,6 +245,9 @@ def main(argv: list[str] | None = None) -> int:
184
  feature_set=args.feature_set,
185
  target=args.target,
186
  source_evidence=source_evidence,
 
 
 
187
  )
188
  best = _fit_select_ridge(
189
  calibration_dataset,
@@ -224,6 +288,9 @@ def main(argv: list[str] | None = None) -> int:
224
  "schema_version": 1,
225
  "k": args.k,
226
  "feature_set": args.feature_set,
 
 
 
227
  "target": args.target,
228
  "fit_objective": args.fit_objective,
229
  "pairwise_weight": args.pairwise_weight,
@@ -248,6 +315,12 @@ def main(argv: list[str] | None = None) -> int:
248
  "eval_target_split_hash": eval_index.get("split_hash"),
249
  "source_content_hash": source_index.get("content_hash"),
250
  "source_split_hash": source_index.get("split_hash"),
 
 
 
 
 
 
251
  "num_calibration_rows": len(calibration_rows),
252
  "num_eval_rows": len(eval_rows),
253
  "num_calibration_candidates": len(calibration_dataset["samples"]),
@@ -304,14 +377,20 @@ def _candidate_dataset(
304
  feature_set: str = "basic",
305
  target: str = "utility_margin",
306
  source_evidence: dict[str, dict[str, Any]] | None = None,
 
 
 
307
  ) -> dict[str, Any]:
308
  source_evidence = source_evidence or {}
 
 
309
  samples: list[dict[str, Any]] = []
310
  by_row: dict[int, list[int]] = defaultdict(list)
311
  for row_index, row in enumerate(rows):
312
  chart_id = str(row.get("chart_id", row.get("group_id", "")))
313
  if chart_id not in charts:
314
  raise KeyError(f"chart_id {chart_id!r} not found in target index")
 
315
  base_score = scorer.base_score(row, charts[chart_id])
316
  scores = [float(value) for value in row.get("predicted_scores", [])[:k]]
317
  utilities = [float(value) for value in row.get("generated_utilities", [])[:k]]
@@ -325,6 +404,7 @@ def _candidate_dataset(
325
  score_mean = sum(scores) / len(scores)
326
  score_std = math.sqrt(sum((score - score_mean) ** 2 for score in scores) / len(scores)) + 1.0e-6
327
  for candidate_index, score in enumerate(scores):
 
328
  tangent = np.asarray(
329
  tangents[candidate_index] if candidate_index < len(tangents) else [],
330
  dtype=float,
@@ -346,12 +426,15 @@ def _candidate_dataset(
346
  tangent=tangent,
347
  source_evidence=_source_evidence_feature(
348
  source_evidence.get(
349
- str(source_chart_ids[candidate_index])
350
- if candidate_index < len(source_chart_ids)
351
- else ""
352
  ),
353
  tangent=tangent,
354
  ),
 
 
 
 
 
355
  num_candidates=len(scores),
356
  feature_set=feature_set,
357
  )
@@ -412,6 +495,8 @@ def _feature_names(feature_set: str) -> list[str]:
412
  names.extend(tangent_names)
413
  if _uses_source_evidence(feature_set):
414
  names.extend(SOURCE_EVIDENCE_NAMES)
 
 
415
  if feature_set in FEATURE_SET_CHOICES:
416
  return names
417
  raise ValueError(f"unknown feature_set: {feature_set}")
@@ -430,6 +515,7 @@ def _candidate_feature(
430
  feature_set: str,
431
  context: dict[str, Any] | None = None,
432
  source_evidence: np.ndarray | None = None,
 
433
  ) -> np.ndarray:
434
  tangent = np.asarray(tangent, dtype=float).reshape(-1)
435
  if tangent.size < 21:
@@ -464,6 +550,10 @@ def _candidate_feature(
464
  if source_evidence is None:
465
  source_evidence = np.zeros(len(SOURCE_EVIDENCE_NAMES), dtype=float)
466
  parts.append(np.asarray(source_evidence, dtype=float).reshape(-1))
 
 
 
 
467
  if feature_set in FEATURE_SET_CHOICES:
468
  return np.concatenate(parts)
469
  raise ValueError(f"unknown feature_set: {feature_set}")
@@ -484,6 +574,8 @@ def _uses_tangent(feature_set: str) -> bool:
484
  "context_tangent",
485
  "tangent_source_evidence",
486
  "context_tangent_source_evidence",
 
 
487
  }
488
 
489
 
@@ -493,9 +585,32 @@ def _uses_source_evidence(feature_set: str) -> bool:
493
  "tangent_source_evidence",
494
  "context_source_evidence",
495
  "context_tangent_source_evidence",
 
 
496
  }
497
 
498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  def _source_evidence_map(index_path: Path) -> tuple[dict[str, dict[str, Any]], dict[str, Any]]:
500
  index_path = _resolve_index_path(index_path)
501
  index = json.loads(index_path.read_text())
@@ -590,6 +705,126 @@ def _source_evidence_feature(source: dict[str, Any] | None, *, tangent: np.ndarr
590
  )
591
 
592
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
  def _clean_array(values: Any) -> np.ndarray:
594
  array = np.asarray(values, dtype=float).reshape(-1)
595
  return array[np.isfinite(array)]
@@ -1080,8 +1315,10 @@ def _write_provenance(out_dir: Path, args: argparse.Namespace) -> None:
1080
  "eval_input": _sha256(args.eval_input),
1081
  "eval_target_index": _sha256(_resolve_index_path(args.eval_target_index)),
1082
  }
1083
- if getattr(args, "source_index", None) is not None:
1084
- hashes["source_index"] = _sha256(_resolve_index_path(args.source_index))
 
 
1085
  (out_dir / "data_hash.txt").write_text(json.dumps(hashes, indent=2, sort_keys=True) + "\n")
1086
  (out_dir / "split_hash.txt").write_text(
1087
  json.dumps(
 
18
 
19
  import numpy as np # noqa: E402
20
 
21
+ from cil.chart_features import CHART_FEATURE_MODES, OBJECT_LAYOUT_EMBED_DIM, OBSERVATION_EMBED_DIM # noqa: E402
22
  from cil.metrics import macro_micro_summary # noqa: E402
23
  from scripts.eval_dominance_selector import _DominanceScorer, _chart_map, _first_train_seed, _rows # noqa: E402
24
+ from scripts.eval_ctt_generated_rollout import load_chart_items # noqa: E402
25
 
26
 
27
  BASIC_FEATURE_NAMES = [
 
58
  "generated_to_source_negative_min_rms",
59
  "generated_source_pos_closer_than_neg",
60
  ]
61
+ CHART_COMPAT_NAMES = [
62
+ "source_chart_feature_found",
63
+ "target_chart_feature_norm",
64
+ "source_chart_feature_norm",
65
+ "target_source_chart_cosine",
66
+ "target_source_chart_rms",
67
+ "target_source_chart_linf",
68
+ "target_source_chart_absmean",
69
+ "target_source_chart_dot_per_dim",
70
+ "target_base_action_norm",
71
+ "source_base_action_norm",
72
+ "target_source_base_cosine",
73
+ "target_source_base_rms",
74
+ "target_source_obs_available",
75
+ "target_source_obs_cosine",
76
+ "target_source_obs_rms",
77
+ "target_obs_norm",
78
+ "source_obs_norm",
79
+ "target_source_obj_available",
80
+ "target_source_obj_cosine",
81
+ "target_source_obj_rms",
82
+ "target_obj_norm",
83
+ "source_obj_norm",
84
+ ]
85
  FEATURE_SET_CHOICES = (
86
  "basic",
87
  "tangent",
 
91
  "tangent_source_evidence",
92
  "context_source_evidence",
93
  "context_tangent_source_evidence",
94
+ "chart_compat",
95
+ "chart_tangent_compat",
96
+ "chart_source_compat",
97
+ "chart_tangent_source_compat",
98
  )
99
 
100
 
 
131
  default="basic",
132
  help="Deployment-visible feature family for candidate-level dominance fitting.",
133
  )
134
+ parser.add_argument(
135
+ "--selector-chart-feature-mode",
136
+ choices=CHART_FEATURE_MODES,
137
+ default="base_context_obs_obj",
138
+ help=(
139
+ "Chart feature mode used only for selector chart-compatibility "
140
+ "features. These maps are loaded without hidden outcomes."
141
+ ),
142
+ )
143
  parser.add_argument(
144
  "--target",
145
  choices=("utility_margin", "success", "success_weighted_margin"),
 
205
  source_evidence, source_index = (
206
  _source_evidence_map(source_index_path) if _uses_source_evidence(args.feature_set) else ({}, {})
207
  )
208
+ selector_source_charts: dict[str, Any] = {}
209
+ selector_source_index: dict[str, Any] = {}
210
+ selector_calibration_charts: dict[str, Any] = {}
211
+ selector_eval_charts: dict[str, Any] = {}
212
+ selector_calibration_index: dict[str, Any] = {}
213
+ selector_eval_index: dict[str, Any] = {}
214
+ if _uses_chart_compat(args.feature_set):
215
+ selector_calibration_charts, selector_calibration_index = _selector_chart_map(
216
+ args.calibration_target_index,
217
+ chart_feature_mode=args.selector_chart_feature_mode,
218
+ )
219
+ selector_eval_charts, selector_eval_index = _selector_chart_map(
220
+ args.eval_target_index,
221
+ chart_feature_mode=args.selector_chart_feature_mode,
222
+ )
223
+ selector_source_charts, selector_source_index = _selector_chart_map(
224
+ source_index_path,
225
+ chart_feature_mode=args.selector_chart_feature_mode,
226
+ )
227
 
228
  calibration_dataset = _candidate_dataset(
229
  calibration_rows,
 
233
  feature_set=args.feature_set,
234
  target=args.target,
235
  source_evidence=source_evidence,
236
+ selector_target_charts=selector_calibration_charts,
237
+ selector_source_charts=selector_source_charts,
238
+ selector_chart_feature_mode=args.selector_chart_feature_mode,
239
  )
240
  eval_dataset = _candidate_dataset(
241
  eval_rows,
 
245
  feature_set=args.feature_set,
246
  target=args.target,
247
  source_evidence=source_evidence,
248
+ selector_target_charts=selector_eval_charts,
249
+ selector_source_charts=selector_source_charts,
250
+ selector_chart_feature_mode=args.selector_chart_feature_mode,
251
  )
252
  best = _fit_select_ridge(
253
  calibration_dataset,
 
288
  "schema_version": 1,
289
  "k": args.k,
290
  "feature_set": args.feature_set,
291
+ "selector_chart_feature_mode": (
292
+ args.selector_chart_feature_mode if _uses_chart_compat(args.feature_set) else None
293
+ ),
294
  "target": args.target,
295
  "fit_objective": args.fit_objective,
296
  "pairwise_weight": args.pairwise_weight,
 
315
  "eval_target_split_hash": eval_index.get("split_hash"),
316
  "source_content_hash": source_index.get("content_hash"),
317
  "source_split_hash": source_index.get("split_hash"),
318
+ "selector_source_content_hash": selector_source_index.get("content_hash"),
319
+ "selector_source_split_hash": selector_source_index.get("split_hash"),
320
+ "selector_calibration_target_content_hash": selector_calibration_index.get("content_hash"),
321
+ "selector_calibration_target_split_hash": selector_calibration_index.get("split_hash"),
322
+ "selector_eval_target_content_hash": selector_eval_index.get("content_hash"),
323
+ "selector_eval_target_split_hash": selector_eval_index.get("split_hash"),
324
  "num_calibration_rows": len(calibration_rows),
325
  "num_eval_rows": len(eval_rows),
326
  "num_calibration_candidates": len(calibration_dataset["samples"]),
 
377
  feature_set: str = "basic",
378
  target: str = "utility_margin",
379
  source_evidence: dict[str, dict[str, Any]] | None = None,
380
+ selector_target_charts: dict[str, Any] | None = None,
381
+ selector_source_charts: dict[str, Any] | None = None,
382
+ selector_chart_feature_mode: str = "base_context_obs_obj",
383
  ) -> dict[str, Any]:
384
  source_evidence = source_evidence or {}
385
+ selector_target_charts = selector_target_charts or {}
386
+ selector_source_charts = selector_source_charts or {}
387
  samples: list[dict[str, Any]] = []
388
  by_row: dict[int, list[int]] = defaultdict(list)
389
  for row_index, row in enumerate(rows):
390
  chart_id = str(row.get("chart_id", row.get("group_id", "")))
391
  if chart_id not in charts:
392
  raise KeyError(f"chart_id {chart_id!r} not found in target index")
393
+ selector_target_chart = selector_target_charts.get(chart_id)
394
  base_score = scorer.base_score(row, charts[chart_id])
395
  scores = [float(value) for value in row.get("predicted_scores", [])[:k]]
396
  utilities = [float(value) for value in row.get("generated_utilities", [])[:k]]
 
404
  score_mean = sum(scores) / len(scores)
405
  score_std = math.sqrt(sum((score - score_mean) ** 2 for score in scores) / len(scores)) + 1.0e-6
406
  for candidate_index, score in enumerate(scores):
407
+ source_chart_id = str(source_chart_ids[candidate_index]) if candidate_index < len(source_chart_ids) else ""
408
  tangent = np.asarray(
409
  tangents[candidate_index] if candidate_index < len(tangents) else [],
410
  dtype=float,
 
426
  tangent=tangent,
427
  source_evidence=_source_evidence_feature(
428
  source_evidence.get(
429
+ source_chart_id
 
 
430
  ),
431
  tangent=tangent,
432
  ),
433
+ chart_compat=_chart_compat_feature(
434
+ selector_target_chart,
435
+ selector_source_charts.get(source_chart_id),
436
+ chart_feature_mode=selector_chart_feature_mode,
437
+ ),
438
  num_candidates=len(scores),
439
  feature_set=feature_set,
440
  )
 
495
  names.extend(tangent_names)
496
  if _uses_source_evidence(feature_set):
497
  names.extend(SOURCE_EVIDENCE_NAMES)
498
+ if _uses_chart_compat(feature_set):
499
+ names.extend(CHART_COMPAT_NAMES)
500
  if feature_set in FEATURE_SET_CHOICES:
501
  return names
502
  raise ValueError(f"unknown feature_set: {feature_set}")
 
515
  feature_set: str,
516
  context: dict[str, Any] | None = None,
517
  source_evidence: np.ndarray | None = None,
518
+ chart_compat: np.ndarray | None = None,
519
  ) -> np.ndarray:
520
  tangent = np.asarray(tangent, dtype=float).reshape(-1)
521
  if tangent.size < 21:
 
550
  if source_evidence is None:
551
  source_evidence = np.zeros(len(SOURCE_EVIDENCE_NAMES), dtype=float)
552
  parts.append(np.asarray(source_evidence, dtype=float).reshape(-1))
553
+ if _uses_chart_compat(feature_set):
554
+ if chart_compat is None:
555
+ chart_compat = np.zeros(len(CHART_COMPAT_NAMES), dtype=float)
556
+ parts.append(np.asarray(chart_compat, dtype=float).reshape(-1))
557
  if feature_set in FEATURE_SET_CHOICES:
558
  return np.concatenate(parts)
559
  raise ValueError(f"unknown feature_set: {feature_set}")
 
574
  "context_tangent",
575
  "tangent_source_evidence",
576
  "context_tangent_source_evidence",
577
+ "chart_tangent_compat",
578
+ "chart_tangent_source_compat",
579
  }
580
 
581
 
 
585
  "tangent_source_evidence",
586
  "context_source_evidence",
587
  "context_tangent_source_evidence",
588
+ "chart_source_compat",
589
+ "chart_tangent_source_compat",
590
  }
591
 
592
 
593
+ def _uses_chart_compat(feature_set: str) -> bool:
594
+ return feature_set in {
595
+ "chart_compat",
596
+ "chart_tangent_compat",
597
+ "chart_source_compat",
598
+ "chart_tangent_source_compat",
599
+ }
600
+
601
+
602
+ def _selector_chart_map(index_path: Path, *, chart_feature_mode: str) -> tuple[dict[str, Any], dict[str, Any]]:
603
+ charts, index = load_chart_items(
604
+ _resolve_index_path(index_path),
605
+ max_charts=None,
606
+ require_positive=False,
607
+ include_hidden=False,
608
+ include_metadata=True,
609
+ chart_feature_mode=chart_feature_mode,
610
+ )
611
+ return {chart.chart_id: chart for chart in charts}, index
612
+
613
+
614
  def _source_evidence_map(index_path: Path) -> tuple[dict[str, dict[str, Any]], dict[str, Any]]:
615
  index_path = _resolve_index_path(index_path)
616
  index = json.loads(index_path.read_text())
 
705
  )
706
 
707
 
708
+ def _chart_compat_feature(
709
+ target_chart: Any | None,
710
+ source_chart: Any | None,
711
+ *,
712
+ chart_feature_mode: str,
713
+ ) -> np.ndarray:
714
+ if target_chart is None or source_chart is None:
715
+ return np.zeros(len(CHART_COMPAT_NAMES), dtype=float)
716
+ target_feature = np.asarray(getattr(target_chart, "feature", []), dtype=float).reshape(-1)
717
+ source_feature = np.asarray(getattr(source_chart, "feature", []), dtype=float).reshape(-1)
718
+ width = min(target_feature.size, source_feature.size)
719
+ if width == 0:
720
+ return np.zeros(len(CHART_COMPAT_NAMES), dtype=float)
721
+ target_feature = target_feature[:width]
722
+ source_feature = source_feature[:width]
723
+ feature_diff = target_feature - source_feature
724
+
725
+ target_base = np.asarray(getattr(target_chart, "base_action", []), dtype=float).reshape(-1)
726
+ source_base = np.asarray(getattr(source_chart, "base_action", []), dtype=float).reshape(-1)
727
+ base_width = min(target_base.size, source_base.size)
728
+ target_base = target_base[:base_width]
729
+ source_base = source_base[:base_width]
730
+ base_diff = target_base - source_base if base_width else np.asarray([], dtype=float)
731
+
732
+ target_segments = _chart_feature_segments(target_chart, chart_feature_mode=chart_feature_mode)
733
+ source_segments = _chart_feature_segments(source_chart, chart_feature_mode=chart_feature_mode)
734
+ target_obs, source_obs = target_segments.get("obs"), source_segments.get("obs")
735
+ target_obj, source_obj = target_segments.get("obj"), source_segments.get("obj")
736
+ obs_available = float(target_obs is not None and source_obs is not None)
737
+ obj_available = float(target_obj is not None and source_obj is not None)
738
+ obs_diff = (
739
+ np.asarray(target_obs, dtype=float) - np.asarray(source_obs, dtype=float)
740
+ if obs_available
741
+ else np.asarray([], dtype=float)
742
+ )
743
+ obj_diff = (
744
+ np.asarray(target_obj, dtype=float) - np.asarray(source_obj, dtype=float)
745
+ if obj_available
746
+ else np.asarray([], dtype=float)
747
+ )
748
+
749
+ return np.asarray(
750
+ [
751
+ 1.0,
752
+ _rms(target_feature),
753
+ _rms(source_feature),
754
+ _cosine(target_feature, source_feature),
755
+ _rms(feature_diff),
756
+ _linf(feature_diff),
757
+ _absmean(feature_diff),
758
+ float(np.dot(target_feature, source_feature) / max(1, width)),
759
+ _rms(target_base),
760
+ _rms(source_base),
761
+ _cosine(target_base, source_base),
762
+ _rms(base_diff),
763
+ obs_available,
764
+ _cosine(target_obs, source_obs) if obs_available else 0.0,
765
+ _rms(obs_diff),
766
+ _rms(target_obs) if obs_available else 0.0,
767
+ _rms(source_obs) if obs_available else 0.0,
768
+ obj_available,
769
+ _cosine(target_obj, source_obj) if obj_available else 0.0,
770
+ _rms(obj_diff),
771
+ _rms(target_obj) if obj_available else 0.0,
772
+ _rms(source_obj) if obj_available else 0.0,
773
+ ],
774
+ dtype=float,
775
+ )
776
+
777
+
778
+ def _chart_feature_segments(chart: Any, *, chart_feature_mode: str) -> dict[str, np.ndarray]:
779
+ feature = np.asarray(getattr(chart, "feature", []), dtype=float).reshape(-1)
780
+ base_action = np.asarray(getattr(chart, "base_action", []), dtype=float).reshape(-1)
781
+ offset = min(base_action.size, feature.size)
782
+ if chart_feature_mode == "base":
783
+ return {}
784
+ context_width = 2 * CONTEXT_HASH_WIDTH + 2
785
+ offset = min(feature.size, offset + context_width)
786
+ segments: dict[str, np.ndarray] = {}
787
+ if chart_feature_mode in {"base_context_obs", "base_context_obs_obj"}:
788
+ end = min(feature.size, offset + OBSERVATION_EMBED_DIM)
789
+ if end - offset == OBSERVATION_EMBED_DIM:
790
+ segments["obs"] = feature[offset:end]
791
+ offset = end
792
+ if chart_feature_mode in {"base_context_obj", "base_context_obs_obj"}:
793
+ end = min(feature.size, offset + OBJECT_LAYOUT_EMBED_DIM)
794
+ if end - offset == OBJECT_LAYOUT_EMBED_DIM:
795
+ segments["obj"] = feature[offset:end]
796
+ return segments
797
+
798
+
799
+ def _rms(values: Any) -> float:
800
+ array = np.asarray(values, dtype=float).reshape(-1)
801
+ return float(np.sqrt(np.mean(array * array))) if array.size else 0.0
802
+
803
+
804
+ def _linf(values: Any) -> float:
805
+ array = np.asarray(values, dtype=float).reshape(-1)
806
+ return float(np.max(np.abs(array))) if array.size else 0.0
807
+
808
+
809
+ def _absmean(values: Any) -> float:
810
+ array = np.asarray(values, dtype=float).reshape(-1)
811
+ return float(np.mean(np.abs(array))) if array.size else 0.0
812
+
813
+
814
+ def _cosine(left: Any, right: Any) -> float:
815
+ left_array = np.asarray(left, dtype=float).reshape(-1)
816
+ right_array = np.asarray(right, dtype=float).reshape(-1)
817
+ width = min(left_array.size, right_array.size)
818
+ if width == 0:
819
+ return 0.0
820
+ left_array = left_array[:width]
821
+ right_array = right_array[:width]
822
+ denom = float(np.linalg.norm(left_array) * np.linalg.norm(right_array))
823
+ if denom <= 1.0e-12:
824
+ return 0.0
825
+ return float(np.dot(left_array, right_array) / denom)
826
+
827
+
828
  def _clean_array(values: Any) -> np.ndarray:
829
  array = np.asarray(values, dtype=float).reshape(-1)
830
  return array[np.isfinite(array)]
 
1315
  "eval_input": _sha256(args.eval_input),
1316
  "eval_target_index": _sha256(_resolve_index_path(args.eval_target_index)),
1317
  }
1318
+ if getattr(args, "source_index", None) is not None or _uses_chart_compat(args.feature_set):
1319
+ hashes["source_index"] = _sha256(
1320
+ _resolve_index_path(args.source_index or args.calibration_target_index)
1321
+ )
1322
  (out_dir / "data_hash.txt").write_text(json.dumps(hashes, indent=2, sort_keys=True) + "\n")
1323
  (out_dir / "split_hash.txt").write_text(
1324
  json.dumps(