File size: 13,523 Bytes
767a051
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
"""Exact rational affine candidate generation and residual evidence."""
from __future__ import annotations

from collections import defaultdict
from itertools import combinations
import hashlib
from typing import Any, Iterable, Mapping

from tom_world03.canonical import attach_hash, canonical_bytes, require_hash
from tom_world03.rational import Q

from .model import Observation, ObservationSet, SPLITS
from .split import SPLIT_CERTIFICATE_SCHEMA

CANDIDATE_SCHEMA = "TOM-AFFINE-HYPOTHESIS-CANDIDATE-0.1"
ENUMERATION_SCHEMA = "TOM-AFFINE-CANDIDATE-ENUMERATION-0.1"
RESIDUAL_EVIDENCE_SCHEMA = "TOM-AFFINE-RESIDUAL-EVIDENCE-0.1"
SELECTION_SCHEMA = "TOM-AFFINE-SELECTION-CERTIFICATE-0.1"
CONTRADICTION_SCHEMA = "TOM-OBSERVATION-CONTRADICTION-0.1"
COUNTEREXAMPLE_SCHEMA = "TOM-AFFINE-COUNTEREXAMPLE-0.1"


def q_complexity(value: Q) -> int:
    """Portable literal complexity in signed numerator/positive denominator bits."""

    sign = 1 if value.numerator < 0 else 0
    return sign + max(1, abs(value.numerator).bit_length()) + value.denominator.bit_length()


def model_complexity(a: Q, b: Q) -> int:
    return q_complexity(a) + q_complexity(b) + int(a != Q(0)) + int(b != Q(0))


def predict(a: Q, b: Q, t: Q) -> Q:
    return a * t + b


def residual(a: Q, b: Q, observation: Observation) -> Q:
    return observation.y - predict(a, b, observation.t)


def _fit_input_record(dataset: ObservationSet, train_ids: Iterable[str]) -> dict[str, Any]:
    by_id = dataset.observation_map()
    ids = sorted(train_ids)
    return {
        "observation_set_id": dataset.id,
        "hypothesis_family_hash": dataset.hypothesis_family.content_hash,
        "training_observations": [
            {
                "id": ident,
                "content_hash": by_id[ident].content_hash,
                "t": by_id[ident].t.to_record(),
                "y": by_id[ident].y.to_record(),
            }
            for ident in ids
        ],
    }


def fit_input_hash(dataset: ObservationSet, train_ids: Iterable[str]) -> str:
    return "sha256:" + hashlib.sha256(canonical_bytes(_fit_input_record(dataset, train_ids))).hexdigest()


def candidate_identity(a: Q, b: Q) -> str:
    basis = {"model": "y=a*t+b", "a": a.to_record(), "b": b.to_record()}
    return "hypothesis:affine:" + hashlib.sha256(canonical_bytes(basis)).hexdigest()[:24]


def derive_candidates(
    dataset: ObservationSet,
    split_certificate: Mapping[str, Any],
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
    require_hash(split_certificate, label="split certificate")
    if split_certificate.get("schema") != SPLIT_CERTIFICATE_SCHEMA:
        raise ValueError("unsupported split certificate")
    if split_certificate.get("observation_set_hash") != dataset.content_hash:
        raise ValueError("split certificate is bound to another observation set")
    train_ids = list(split_certificate.get("splits", {}).get("train", []))
    by_id = dataset.observation_map()
    try:
        training = [by_id[ident] for ident in train_ids]
    except KeyError as exc:
        raise ValueError(f"split references unknown training observation {exc.args[0]}") from exc
    if len(training) < 2:
        raise ValueError("affine fitting requires at least two training observations")

    grouped: dict[tuple[Q, Q], list[tuple[str, str]]] = defaultdict(list)
    pair_count = 0
    for left, right in combinations(sorted(training, key=lambda item: item.id), 2):
        if left.t == right.t:
            continue
        pair_count += 1
        a = (right.y - left.y) / (right.t - left.t)
        b = left.y - a * left.t
        grouped[(a, b)].append((left.id, right.id))
        if len(grouped) > dataset.hypothesis_family.max_candidates:
            raise ValueError("affine candidate budget exceeded")

    fit_hash = fit_input_hash(dataset, train_ids)
    candidates: list[dict[str, Any]] = []
    for (a, b), pairs in grouped.items():
        pairs = sorted(set(tuple(sorted(pair)) for pair in pairs))
        candidate = attach_hash({
            "schema": CANDIDATE_SCHEMA,
            "profile": "TOM-LEARNER-0.1",
            "id": candidate_identity(a, b),
            "observation_set_id": dataset.id,
            "hypothesis_family_hash": dataset.hypothesis_family.content_hash,
            "split_certificate_hash": split_certificate["content_hash"],
            "fit_input_hash": fit_hash,
            "fit_uses_splits": ["train"],
            "model": "y=a*t+b",
            "coefficients": {"a": a.to_record(), "b": b.to_record()},
            "supporting_pairs": [[left, right] for left, right in pairs],
            "complexity": model_complexity(a, b),
        })
        candidates.append(candidate)

    def order_key(candidate: Mapping[str, Any]) -> tuple[Any, ...]:
        coeffs = candidate["coefficients"]
        return (
            int(candidate["complexity"]),
            Q.from_value(coeffs["a"]),
            Q.from_value(coeffs["b"]),
            str(candidate["id"]),
        )

    candidates.sort(key=order_key)
    enumeration = attach_hash({
        "schema": ENUMERATION_SCHEMA,
        "profile": "TOM-LEARNER-0.1",
        "observation_set_id": dataset.id,
        "observation_set_hash": dataset.content_hash,
        "split_certificate_hash": split_certificate["content_hash"],
        "hypothesis_family_hash": dataset.hypothesis_family.content_hash,
        "fit_input_hash": fit_hash,
        "fit_uses_splits": ["train"],
        "training_observation_ids": sorted(train_ids),
        "distinct_t_pair_count": pair_count,
        "candidate_count": len(candidates),
        "candidate_order": [candidate["content_hash"] for candidate in candidates],
        "candidate_ids": [candidate["id"] for candidate in candidates],
    })
    return candidates, enumeration


def candidate_coefficients(candidate: Mapping[str, Any]) -> tuple[Q, Q]:
    require_hash(candidate, label="affine candidate")
    if candidate.get("schema") != CANDIDATE_SCHEMA:
        raise ValueError("unsupported affine candidate schema")
    coeffs = candidate.get("coefficients")
    if not isinstance(coeffs, Mapping):
        raise ValueError("candidate coefficients must be an object")
    return Q.from_value(coeffs.get("a")), Q.from_value(coeffs.get("b"))


def _metrics(residuals: list[Q]) -> dict[str, Any]:
    absolute = [abs(value) for value in residuals]
    squared = [value * value for value in residuals]
    maximum = max(absolute, default=Q(0))
    return {
        "count": len(residuals),
        "zero_count": sum(value == Q(0) for value in residuals),
        "nonzero_count": sum(value != Q(0) for value in residuals),
        "max_abs_residual": maximum.to_record(),
        "sum_abs_residual": sum(absolute, Q(0)).to_record(),
        "sum_squared_residual": sum(squared, Q(0)).to_record(),
        "exact": all(value == Q(0) for value in residuals),
    }


def evaluate_candidate(
    dataset: ObservationSet,
    split_certificate: Mapping[str, Any],
    candidate: Mapping[str, Any],
) -> dict[str, Any]:
    a, b = candidate_coefficients(candidate)
    if candidate.get("observation_set_id") != dataset.id:
        raise ValueError("candidate belongs to another observation set")
    if candidate.get("split_certificate_hash") != split_certificate.get("content_hash"):
        raise ValueError("candidate belongs to another split")
    by_id = dataset.observation_map()
    split_records: dict[str, Any] = {}
    for name in SPLITS:
        ids = list(split_certificate.get("splits", {}).get(name, []))
        rows: list[dict[str, Any]] = []
        values: list[Q] = []
        for ident in ids:
            observation = by_id[ident]
            predicted = predict(a, b, observation.t)
            value = observation.y - predicted
            values.append(value)
            rows.append({
                "observation_id": observation.id,
                "observation_hash": observation.content_hash,
                "t": observation.t.to_record(),
                "observed_y": observation.y.to_record(),
                "predicted_y": predicted.to_record(),
                "residual": value.to_record(),
            })
        split_records[name] = {
            "metrics": _metrics(values),
            "residuals": rows,
        }
    return attach_hash({
        "schema": RESIDUAL_EVIDENCE_SCHEMA,
        "profile": "TOM-LEARNER-0.1",
        "observation_set_id": dataset.id,
        "observation_set_hash": dataset.content_hash,
        "split_certificate_hash": split_certificate["content_hash"],
        "candidate_id": candidate["id"],
        "candidate_hash": candidate["content_hash"],
        "fit_input_hash": candidate["fit_input_hash"],
        "splits": split_records,
    })


def training_rank(candidate: Mapping[str, Any], evidence: Mapping[str, Any]) -> tuple[Any, ...]:
    metrics = evidence["splits"]["train"]["metrics"]
    return (
        int(metrics["nonzero_count"]),
        Q.from_value(metrics["max_abs_residual"]),
        Q.from_value(metrics["sum_abs_residual"]),
        int(candidate["complexity"]),
        Q.from_value(candidate["coefficients"]["a"]),
        Q.from_value(candidate["coefficients"]["b"]),
        str(candidate["id"]),
    )


def select_from_training(
    dataset: ObservationSet,
    split_certificate: Mapping[str, Any],
    candidates: list[Mapping[str, Any]],
    evidence: list[Mapping[str, Any]],
) -> dict[str, Any]:
    if len(candidates) != len(evidence):
        raise ValueError("candidate and evidence counts differ")
    pairs = sorted(zip(candidates, evidence), key=lambda pair: training_rank(pair[0], pair[1]))
    exact = [
        (candidate, proof)
        for candidate, proof in pairs
        if proof["splits"]["train"]["metrics"]["exact"]
    ]
    selected: Mapping[str, Any] | None = None
    reason = ""
    if not pairs:
        reason = "no candidate can be derived from distinct training inputs"
    elif not exact:
        reason = "no candidate has zero residual on every training observation"
    elif len(exact) > 1 and dataset.acceptance_policy.require_unique_exact_train:
        reason = "multiple semantically distinct exact training candidates survive"
    else:
        selected = exact[0][0]
        reason = "unique exact training candidate selected without validation or holdout input"
    best = pairs[0][0] if pairs else None
    return attach_hash({
        "schema": SELECTION_SCHEMA,
        "profile": "TOM-LEARNER-0.1",
        "observation_set_id": dataset.id,
        "observation_set_hash": dataset.content_hash,
        "split_certificate_hash": split_certificate["content_hash"],
        "acceptance_policy_hash": dataset.acceptance_policy.content_hash,
        "selection_uses_splits": ["train"],
        "candidate_count": len(candidates),
        "exact_training_candidate_count": len(exact),
        "ranked_candidate_hashes": [candidate["content_hash"] for candidate, _ in pairs],
        "best_training_candidate_hash": None if best is None else best["content_hash"],
        "selected_candidate_hash": None if selected is None else selected["content_hash"],
        "selected_candidate_id": None if selected is None else selected["id"],
        "reason": reason,
    })


def find_contradictions(dataset: ObservationSet) -> list[dict[str, Any]]:
    grouped: dict[Q, list[Observation]] = defaultdict(list)
    for observation in dataset.observations:
        grouped[observation.t].append(observation)
    result: list[dict[str, Any]] = []
    for time, observations in grouped.items():
        outputs = sorted({observation.y for observation in observations})
        if len(outputs) <= 1:
            continue
        record = attach_hash({
            "schema": CONTRADICTION_SCHEMA,
            "profile": "TOM-LEARNER-0.1",
            "observation_set_id": dataset.id,
            "observation_set_hash": dataset.content_hash,
            "input": time.to_record(),
            "distinct_outputs": [value.to_record() for value in outputs],
            "observations": [
                {"id": item.id, "content_hash": item.content_hash, "y": item.y.to_record()}
                for item in sorted(observations, key=lambda item: item.id)
            ],
            "meaning": "the same exact input carries more than one exact output",
        })
        result.append(record)
    return result


def counterexamples_from_evidence(
    dataset: ObservationSet,
    evidence: Mapping[str, Any],
) -> list[dict[str, Any]]:
    result: list[dict[str, Any]] = []
    for split_name in SPLITS:
        for row in evidence["splits"][split_name]["residuals"]:
            value = Q.from_value(row["residual"])
            if value == Q(0):
                continue
            result.append(attach_hash({
                "schema": COUNTEREXAMPLE_SCHEMA,
                "profile": "TOM-LEARNER-0.1",
                "observation_set_id": dataset.id,
                "observation_set_hash": dataset.content_hash,
                "candidate_hash": evidence["candidate_hash"],
                "evidence_hash": evidence["content_hash"],
                "split": split_name,
                "observation_id": row["observation_id"],
                "observation_hash": row["observation_hash"],
                "t": row["t"],
                "observed_y": row["observed_y"],
                "predicted_y": row["predicted_y"],
                "residual": row["residual"],
            }))
    result.sort(key=lambda item: (SPLITS.index(item["split"]), item["observation_id"]))
    return result