TOMK / src /python /tom_learner06 /oracle.py
c0mbatduckzz's picture
Publish WQK 0.6 formal specification, use cases and verified release (part 2)
767a051 verified
Raw History Blame Contribute Delete
10.2 kB
from __future__ import annotations
"""Independent exact oracle for the finite Learner 0.2 registry.
This module deliberately does not invoke :mod:`tomagi.formal` or reuse the
formal expression tree. It is a separately implemented falsification oracle
using :class:`fractions.Fraction` and ordinary finite enumeration.
"""
from dataclasses import dataclass
from fractions import Fraction
import copy
from typing import Any, Iterable, Mapping
from tomagi.canonical import attach_hash, content_hash, verify_hash
class OracleError(ValueError):
pass
def frac(value: Any) -> Fraction:
if isinstance(value, bool):
raise OracleError("boolean is not an exact rational")
if isinstance(value, int):
return Fraction(value, 1)
if isinstance(value, Mapping) and set(value) == {"num", "den"}:
n, d = value["num"], value["den"]
if isinstance(n, bool) or isinstance(d, bool) or not isinstance(n, int) or not isinstance(d, int):
raise OracleError("rational words must be integers")
if d <= 0:
raise OracleError("rational denominator must be positive")
f = Fraction(n, d)
if f.numerator != n or f.denominator != d:
raise OracleError("rational must be reduced")
return f
raise OracleError(f"not an exact rational: {value!r}")
def q(value: Fraction | int) -> dict[str, int]:
f = value if isinstance(value, Fraction) else Fraction(value, 1)
return {"num": f.numerator, "den": f.denominator}
def _eval_tree(node: Mapping[str, Any], x: Fraction, depth: int = 0) -> Fraction:
if depth > 2:
raise OracleError("expression tree exceeds depth 2")
op = node.get("op")
if op == "x":
return x
if op == "const":
return frac(node["value"])
if op in {"neg", "abs", "square"}:
value = _eval_tree(node["arg"], x, depth + 1)
if op == "neg":
return -value
if op == "abs":
return abs(value)
return value * value
if op in {"add", "sub", "mul"}:
left = _eval_tree(node["left"], x, depth + 1)
right = _eval_tree(node["right"], x, depth + 1)
if op == "add":
return left + right
if op == "sub":
return left - right
return left * right
raise OracleError(f"unknown expression-tree op {op!r}")
def predict(candidate: Mapping[str, Any], observation: Mapping[str, Any]) -> tuple[bool, Any]:
family = candidate.get("family_id")
x_value = observation.get("input")
if family == "family:polynomial:0.2":
x = frac(x_value)
coeffs = candidate["coefficients"]
return True, q(frac(coeffs[0]) + frac(coeffs[1]) * x + frac(coeffs[2]) * x * x)
if family == "family:piecewise-affine:0.2":
x = frac(x_value)
side = candidate["left"] if x <= frac(candidate["breakpoint"]) else candidate["right"]
return True, q(frac(side["slope"]) * x + frac(side["intercept"]))
if family == "family:transition-table:0.2":
matches = [entry for entry in candidate["entries"] if entry["input"] == x_value]
if len(matches) != 1:
return False, None
return True, copy.deepcopy(matches[0]["output"])
if family == "family:expression-tree:0.2":
return True, q(_eval_tree(candidate["tree"], frac(x_value)))
raise OracleError(f"unknown candidate family {family!r}")
def _matches(candidate: Mapping[str, Any], observation: Mapping[str, Any]) -> bool:
defined, value = predict(candidate, observation)
if not defined:
return False
target = observation["target"]
numeric = isinstance(target, int) or isinstance(target, Mapping)
if numeric:
try:
return frac(value) == frac(target)
except OracleError:
return False
return value == target
def _partition(dataset: Mapping[str, Any], name: str) -> list[Mapping[str, Any]]:
ids = dataset["partitions"][name]
by_id = {observation["id"]: observation for observation in dataset["observations"]}
return [by_id[item] for item in ids]
def _contradictions(observations: Iterable[Mapping[str, Any]]) -> list[dict[str, Any]]:
rows = list(observations)
out: list[dict[str, Any]] = []
for left_index, left in enumerate(rows):
for right in rows[left_index + 1:]:
if left["input"] == right["input"] and left["target"] != right["target"]:
out.append(attach_hash({
"schema": "TOM-LEARNER-0.2-CONTRADICTION-1.0",
"left_observation": left["content_hash"],
"right_observation": right["content_hash"],
"input": copy.deepcopy(left["input"]),
"left_target": copy.deepcopy(left["target"]),
"right_target": copy.deepcopy(right["target"]),
}))
return out
def _family_candidates(registry: Mapping[str, Any], dataset: Mapping[str, Any]) -> list[Mapping[str, Any]]:
eligible = set(dataset["eligible_families"])
out: list[Mapping[str, Any]] = []
for family in registry["families"]:
if family["id"] in eligible:
if len(family["candidates"]) > family["search_budget"]["max_candidates"]:
raise OracleError("family candidate registry exceeds declared search budget")
out.extend(family["candidates"])
return out
def _fit_hash(dataset: Mapping[str, Any], registry: Mapping[str, Any]) -> str:
train = _partition(dataset, "train")
return content_hash({
"schema": "TOM-LEARNER-0.2-TRAIN-FIT-INPUT-1.0",
"dataset_id": dataset["id"],
"family_registry_hash": registry["content_hash"],
"eligible_families": dataset["eligible_families"],
"train_observations": [item["content_hash"] for item in train],
})
def _regression_impact(selected: Mapping[str, Any] | None, dataset: Mapping[str, Any],
prior: Mapping[str, Any]) -> dict[str, Any]:
supersedes = dataset.get("supersedes")
found = supersedes is None
results: list[dict[str, Any]] = []
for definition in prior["definitions"]:
replacement = definition["model"]
replaced = supersedes == definition["content_hash"]
if replaced:
found = True
if selected is not None:
replacement = selected
case_results = []
for case in definition["regression_cases"]:
observation = {"input": case["input"], "target": case["expected"]}
passed = _matches(replacement, observation)
case_results.append({"case_id": case["id"], "passed": passed})
results.append({
"definition_hash": definition["content_hash"],
"replaced": replaced,
"passed": all(item["passed"] for item in case_results),
"cases": case_results,
})
all_pass = found and all(item["passed"] for item in results)
return attach_hash({
"schema": "TOM-LEARNER-0.2-REGRESSION-IMPACT-1.0",
"dataset_id": dataset["id"],
"supersedes": supersedes,
"superseded_definition_found": found,
"tested_definitions": len(results),
"all_pass": all_pass,
"results": results,
})
def evaluate_dataset(dataset: Mapping[str, Any], registry: Mapping[str, Any],
prior: Mapping[str, Any]) -> dict[str, Any]:
if not verify_hash(dict(dataset)) or not verify_hash(dict(registry)) or not verify_hash(dict(prior)):
raise OracleError("oracle input content hash mismatch")
train = _partition(dataset, "train")
validation = _partition(dataset, "validation")
holdout = _partition(dataset, "holdout")
candidates = _family_candidates(registry, dataset)
survivors = [candidate for candidate in candidates if all(_matches(candidate, row) for row in train)]
contradictions = _contradictions(dataset["observations"])
selected = survivors[0] if len(survivors) == 1 else None
validation_failures = [] if selected is None else [row for row in validation if not _matches(selected, row)]
holdout_failures = [] if selected is None else [row for row in holdout if not _matches(selected, row)]
regression = _regression_impact(selected, dataset, prior)
accepted = (
len(survivors) == 1
and not contradictions
and not validation_failures
and not holdout_failures
and regression["all_pass"]
)
if len(survivors) > 1:
reason = "ambiguous-train-survivors"
elif len(survivors) == 0:
reason = "no-exact-train-candidate"
elif contradictions:
reason = "contradiction"
elif validation_failures:
reason = "validation-counterexample"
elif holdout_failures:
reason = "holdout-counterexample"
elif not regression["all_pass"]:
reason = "regression-impact"
else:
reason = "accepted"
return {
"dataset_id": dataset["id"],
"accepted": accepted,
"reason": reason,
"candidate_count": len(candidates),
"survivor_hashes": [item["content_hash"] for item in survivors],
"selected_candidate_hash": None if selected is None else selected["content_hash"],
"fit_input_hash": _fit_hash(dataset, registry),
"validation_failures": [item["id"] for item in validation_failures],
"holdout_failures": [item["id"] for item in holdout_failures],
"contradiction_count": len(contradictions),
"regression_all_pass": regression["all_pass"],
"supersedes": dataset.get("supersedes"),
}
def evaluate_all(datasets: list[Mapping[str, Any]], registry: Mapping[str, Any],
prior: Mapping[str, Any]) -> dict[str, Any]:
rows = [evaluate_dataset(dataset, registry, prior) for dataset in datasets]
return {
"dataset_count": len(rows),
"accepted_count": sum(row["accepted"] for row in rows),
"rejected_count": sum(not row["accepted"] for row in rows),
"ambiguous_count": sum(row["reason"] == "ambiguous-train-survivors" for row in rows),
"rows": rows,
}