anhtld commited on
Commit
0c7ffe8
·
verified ·
1 Parent(s): 508df95

manual-sync chart-synthesis 2026-07-02T22:45:08Z workspace/dovla_cil/generation/tangent_chart_synthesis.py

Browse files
workspace/dovla_cil/generation/tangent_chart_synthesis.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from collections import Counter
5
+ from collections.abc import Sequence
6
+ from dataclasses import asdict, dataclass, field
7
+ from typing import Any
8
+
9
+ from dovla_cil.generation.tangent_spline_cvae import (
10
+ SplineTangentRow,
11
+ _aggregate_rows,
12
+ _evaluate_generated_group,
13
+ _split_rows,
14
+ _validate_eval_args,
15
+ build_spline_tangent_rows,
16
+ )
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class ChartSynthesisConfig:
21
+ obs_dim: int = 96
22
+ text_dim: int = 64
23
+ val_fraction: float = 0.2
24
+ seed: int = 0
25
+ neighbor_pool: int = 64
26
+ direct_count: int = 12
27
+ barycentric_windows: tuple[int, ...] = field(default_factory=lambda: (4, 8, 16, 32))
28
+ barycentric_temperature: float = 0.5
29
+ utility_weight: float = 0.0
30
+ order: str = "direct_first"
31
+ same_task_only: bool = True
32
+
33
+ def __post_init__(self) -> None:
34
+ if self.obs_dim <= 0:
35
+ raise ValueError("obs_dim must be positive")
36
+ if self.text_dim <= 0:
37
+ raise ValueError("text_dim must be positive")
38
+ if not 0.0 < self.val_fraction < 1.0:
39
+ raise ValueError("val_fraction must be in (0, 1)")
40
+ if self.neighbor_pool <= 0:
41
+ raise ValueError("neighbor_pool must be positive")
42
+ if self.direct_count < 0:
43
+ raise ValueError("direct_count must be non-negative")
44
+ if any(window <= 0 for window in self.barycentric_windows):
45
+ raise ValueError("barycentric_windows must contain positive values")
46
+ if self.barycentric_temperature <= 0.0:
47
+ raise ValueError("barycentric_temperature must be positive")
48
+ if self.utility_weight < 0.0:
49
+ raise ValueError("utility_weight must be non-negative")
50
+ if self.order not in {"direct_first", "means_first", "interleave"}:
51
+ raise ValueError("order must be direct_first, means_first, or interleave")
52
+
53
+
54
+ def evaluate_chart_synthesis(
55
+ targets: Sequence[dict[str, Any]],
56
+ *,
57
+ config: ChartSynthesisConfig | None = None,
58
+ k_values: Sequence[int] = (1, 2, 4, 8, 16),
59
+ thresholds: Sequence[float] = (0.05, 0.1, 0.2, 0.4),
60
+ ) -> dict[str, Any]:
61
+ """Evaluate local chart synthesis from train-only positive tangents.
62
+
63
+ Local-atlas retrieval asks whether a heldout positive tangent already appears
64
+ among nearby train positives. This diagnostic takes one step toward a
65
+ generator: it keeps a few nearest chart atoms, then adds barycentric means
66
+ over increasingly wide local neighborhoods. These means are still
67
+ deployment-clean because they use only train positives and the current
68
+ observation-language condition.
69
+ """
70
+
71
+ config = config or ChartSynthesisConfig()
72
+ _validate_eval_args(k_values, thresholds)
73
+ rows, task_ids, code_dim, horizon, action_dim = build_spline_tangent_rows(
74
+ targets,
75
+ obs_dim=config.obs_dim,
76
+ text_dim=config.text_dim,
77
+ )
78
+ if not rows:
79
+ raise ValueError("no usable spline tangent rows")
80
+ split = _split_rows(rows, val_fraction=config.val_fraction, seed=config.seed)
81
+ train_positive = [
82
+ row for row in split["train"]
83
+ if row.example.tangent_label == "positive"
84
+ ]
85
+ if not train_positive:
86
+ raise ValueError("no train positive tangents")
87
+ train_positive_by_task: dict[str, list[SplineTangentRow]] = {}
88
+ for row in train_positive:
89
+ train_positive_by_task.setdefault(row.example.task_id, []).append(row)
90
+
91
+ max_k = max(int(k) for k in k_values)
92
+ val_by_group: dict[str, list[SplineTangentRow]] = {}
93
+ for row in split["val"]:
94
+ val_by_group.setdefault(row.example.group_id, []).append(row)
95
+
96
+ groups: list[dict[str, Any]] = []
97
+ for group_id, group_rows in sorted(val_by_group.items()):
98
+ positives = [
99
+ row.example for row in group_rows
100
+ if row.example.tangent_label == "positive"
101
+ ]
102
+ if not positives:
103
+ continue
104
+ negatives = [
105
+ row.example for row in group_rows
106
+ if row.example.tangent_label == "negative"
107
+ ]
108
+ proposals = _select_chart_proposals(
109
+ group_rows[0],
110
+ train_positive=train_positive,
111
+ train_positive_by_task=train_positive_by_task,
112
+ config=config,
113
+ max_k=max_k,
114
+ )
115
+ groups.append(
116
+ _evaluate_generated_group(
117
+ group_id,
118
+ task_id=group_rows[0].example.task_id,
119
+ proposals=proposals,
120
+ positives=positives,
121
+ negatives=negatives,
122
+ k_values=k_values,
123
+ thresholds=thresholds,
124
+ )
125
+ )
126
+
127
+ return {
128
+ "report_type": "positive_tangent_chart_synthesis_eval",
129
+ "metric_scope": "offline_support_proxy",
130
+ "note": (
131
+ "Chart synthesis proposes train-only positive tangents from local "
132
+ "atlas neighborhoods plus barycentric chart means. It tests whether "
133
+ "local positive support is better represented as chart coordinates "
134
+ "than as raw prototype replay."
135
+ ),
136
+ "config": {
137
+ **asdict(config),
138
+ "barycentric_windows": list(config.barycentric_windows),
139
+ },
140
+ "task_ids": list(task_ids),
141
+ "code_dim": code_dim,
142
+ "horizon": horizon,
143
+ "action_dim": action_dim,
144
+ "num_examples": len(rows),
145
+ "num_groups": len({row.example.group_id for row in rows}),
146
+ "num_train_examples": len(split["train"]),
147
+ "num_val_examples": len(split["val"]),
148
+ "num_train_positive": len(train_positive),
149
+ "num_val_groups_with_positive": len(groups),
150
+ "label_counts": dict(Counter(row.example.tangent_label for row in rows)),
151
+ "train_positive_by_task": {
152
+ task_id: len(train_positive_by_task.get(task_id, []))
153
+ for task_id in task_ids
154
+ },
155
+ "overall": _aggregate_rows(groups, k_values=k_values, thresholds=thresholds),
156
+ "per_task": {
157
+ task_id: _aggregate_rows(
158
+ [group for group in groups if group["task_id"] == task_id],
159
+ k_values=k_values,
160
+ thresholds=thresholds,
161
+ )
162
+ for task_id in task_ids
163
+ },
164
+ "groups": groups,
165
+ }
166
+
167
+
168
+ def _select_chart_proposals(
169
+ query: SplineTangentRow,
170
+ *,
171
+ train_positive: Sequence[SplineTangentRow],
172
+ train_positive_by_task: dict[str, list[SplineTangentRow]],
173
+ config: ChartSynthesisConfig,
174
+ max_k: int,
175
+ ) -> list[list[float]]:
176
+ if config.same_task_only:
177
+ pool = train_positive_by_task.get(query.example.task_id, []) or list(train_positive)
178
+ else:
179
+ pool = list(train_positive)
180
+ ranked = sorted(
181
+ (
182
+ (_condition_distance(query.condition, row.condition), row)
183
+ for row in pool
184
+ ),
185
+ key=lambda item: (item[0], -item[1].example.delta_utility, item[1].example.group_id),
186
+ )[: max(int(config.neighbor_pool), max_k)]
187
+ direct_rows = [row for _, row in ranked[: int(config.direct_count)]]
188
+ direct = [_flat_delta(row) for row in direct_rows]
189
+ means = [
190
+ _weighted_barycenter(
191
+ ranked[: min(int(window), len(ranked))],
192
+ temperature=config.barycentric_temperature,
193
+ utility_weight=config.utility_weight,
194
+ )
195
+ for window in config.barycentric_windows
196
+ if ranked
197
+ ]
198
+ means = [mean for mean in means if mean]
199
+ if config.order == "means_first":
200
+ proposals = means + direct
201
+ elif config.order == "interleave":
202
+ proposals = []
203
+ for index in range(max(len(direct), len(means))):
204
+ if index < len(direct):
205
+ proposals.append(direct[index])
206
+ if index < len(means):
207
+ proposals.append(means[index])
208
+ else:
209
+ proposals = direct + means
210
+
211
+ used_direct = len(direct)
212
+ while len(proposals) < max_k and used_direct < len(ranked):
213
+ proposals.append(_flat_delta(ranked[used_direct][1]))
214
+ used_direct += 1
215
+ return proposals[:max_k]
216
+
217
+
218
+ def _weighted_barycenter(
219
+ ranked: Sequence[tuple[float, SplineTangentRow]],
220
+ *,
221
+ temperature: float,
222
+ utility_weight: float,
223
+ ) -> list[float]:
224
+ if not ranked:
225
+ return []
226
+ nonzero = sorted(distance for distance, _ in ranked if distance > 1.0e-9)
227
+ local_scale = nonzero[len(nonzero) // 2] if nonzero else 1.0
228
+ scale = max(local_scale * float(temperature), 1.0e-6)
229
+ scores = [
230
+ -float(distance) / scale + float(utility_weight) * row.example.delta_utility
231
+ for distance, row in ranked
232
+ ]
233
+ max_score = max(scores)
234
+ weights = [math.exp(score - max_score) for score in scores]
235
+ total = sum(weights) or 1.0
236
+ vectors = [_flat_delta(row) for _, row in ranked]
237
+ size = min(len(vector) for vector in vectors)
238
+ return [
239
+ sum(weight * vector[index] for weight, vector in zip(weights, vectors, strict=True))
240
+ / total
241
+ for index in range(size)
242
+ ]
243
+
244
+
245
+ def _condition_distance(left: Sequence[float], right: Sequence[float]) -> float:
246
+ return _rms_distance(left, right)
247
+
248
+
249
+ def _flat_delta(row: SplineTangentRow) -> list[float]:
250
+ return row.example.flat_delta
251
+
252
+
253
+ def _rms_distance(left: Sequence[float], right: Sequence[float]) -> float:
254
+ size = min(len(left), len(right))
255
+ if size <= 0:
256
+ return 0.0
257
+ return math.sqrt(
258
+ sum((float(left[index]) - float(right[index])) ** 2 for index in range(size))
259
+ / size
260
+ )