Can it spot an ordinary value in the wrong context?

Reconstruction
Learn to reconstruct a numeric answer from its context. Use a large reconstruction error as a warning that the value does not fit.

Example

group: amber
x: 1.0
y: 0.24

The answer is shown here for explanation; it is hidden from the model when scored.

Model tree with record, group, x, y. Amber cards are hidden prediction targets. Other fields provide the input.

Model tree with record, group, x, y. Amber cards are hidden prediction targets. Other fields provide the input.

Figure 1: Amber cards are hidden prediction targets. Other fields provide the input.

Comparison

Shuffle answers among test records so their individual distribution stays unchanged. Compare with an independently shuffled-training model.

Result

P076 · Latest full run · Passing

Seed 7603 on cpu: 11 of 11 test checks met. History: 3 of 3 full runs met every check, across 3 distinct seeds. History can include earlier source versions. The experiment code has changed since this run; these measurements describe its earlier version.

Comparison Latest measurement
Learned context 0.9624
Shuffled training 0.5017
False alarms on normal records 5.0%

These are AUC ranking scores: 0.5 is chance and 1 is perfect separation. They are not percentages of correct answers.

All three seeds detect this constructed inconsistency. The warning score is not a calibrated anomaly probability, and rare valid values need their own false-alarm check.

Experiment details and code

Latest full run

Seed 7603, cpu, recorded 2026-09-22T17:14:14.502615+00:00. Outcome: met.

Source fingerprint: 723df67acfc271cdf7966e1636fbe160109fe640c6b9b7ec51180ea402388683. Python 3.12.10; Torch 2.12.0.

Measurement Value
train_rows 4096
test_rows_per_condition 2048
marginal_auc 0.5
informative steps: 500; validation_threshold: 0.214064; rmse: 0.108364; baseline_rmse: 1.28562; normalized_rmse: 0.0842897; anomaly_auc: 0.962365; detection_recall: 0.90625; normal_false_positive_rate: 0.050293; tail_false_positive_rate: 0.0537109
shuffled_training steps: 500; validation_threshold: 2.39045; rmse: 1.28429; baseline_rmse: 1.28562; normalized_rmse: 0.998972; anomaly_auc: 0.50166; detection_recall: 0.0458984; normal_false_positive_rate: 0.0444336; tail_false_positive_rate: 0.290527
Behavioral checks
Behavioral check Outcome
Anomalies preserve the exact target marginal Met
Anomalies preserve visible contexts Met
Marginal deviation baseline has exactly chance AUROC Met
informative: every measurement is finite Met
Informative reconstruction nRMSE below 0.20 Met
Contextual anomaly AUROC reaches 0.90 Met
Anomaly recall reaches 0.75 Met
Normal false positive rate at most 0.10 Met
Rare valid tail false positive rate at most 0.15 Met
shuffled_training: every measurement is finite Met
Shuffled training anomaly AUROC stays below 0.65 Met

Full run history

Runs may include earlier source versions. Repeating a seed is not an independent seed check.

Seed Outcome Recorded Code fingerprint
7601 met 2026-09-22T17:13:38.626175+00:00 78fcaa10ddfaad2e4e525847df8db6837c9bb286f196995a5c4085fdde0f8c72
7602 met 2026-09-22T17:13:56.719280+00:00 78fcaa10ddfaad2e4e525847df8db6837c9bb286f196995a5c4085fdde0f8c72
7603 met 2026-09-22T17:14:14.502615+00:00 78fcaa10ddfaad2e4e525847df8db6837c9bb286f196995a5c4085fdde0f8c72

Recorded results.

Run this experiment

Proof execution is optional and separate from building this site. Use a RelFlow build matching the recorded provenance; replace the wheel path below with that artifact. Run from this documentation repository’s root:

uv run --with /path/to/matching-relflow.whl python proofs/run.py P076

Or, from a prepared proof environment, run the self-contained script directly:

PYTHONPATH=proofs python proofs/anomaly/contextual_consistency.py

Add --accelerator gpu for CUDA or --seed 42 for another seeded experiment. --steps 2 checks execution with a short training budget; it is recorded as a smoke run.

Download the complete proof.

Process, examples, and observability

Draw a balanced group and x ~ Uniform(-2, 2). Set y = slope[group] * x + offset[group] + Normal(0, 0.1). The model observes only group and x when reconstructing y. Slopes and offsets are evaluator constants, never model inputs. Each split has its own random stream, with run seeds spaced by 100 before assigning stream offsets.

Actual test anomalies permute y between records, preserving its entire distribution. Valid-tail tests restrict absolute x to [1.8, 2], within training support.

Training, scoring, and gates

Fit informative and shuffled-y models from the same initialization for 500 AdamW updates each, batch size 128 and learning rate 0.002. Use 4,096 training rows, 2,048 clean validation rows, and separate 2,048-row test and tail sets. Select each model’s residual threshold at the clean validation 95th percentile. No test observations choose that threshold or a checkpoint.

Predeclared capability gates require informative test nRMSE below 0.20, anomaly AUROC at least 0.90, detection recall at least 0.75, normal false positives at most 0.10, and tail false positives at most 0.15. Shuffled training must leave AUROC at most 0.65. A univariate y-deviation baseline scores exactly 0.5 AUROC because its two score distributions are identical.

Complete experiment code

Code
"""P076: detect broken cross-field relationships using hidden-field reconstruction."""

from collections.abc import Iterator
from functools import partial

import lightning.pytorch as lit
import numpy as np
import pyarrow as pa
from reporting import report

import relflow as rf

PROOF_ID = "P076"
BUDGET = 500
GROUPS = ("amber", "birch", "cove", "dune")
SLOPES = np.array([0.7, -0.8, 1.2, -1.3])
OFFSETS = np.array([-0.5, 0.5, -0.3, 0.3])
TRAIN_ROWS = 4096
TEST_ROWS = 2048


def records(*, rows: int, seed: int, shuffled: bool = False, tail: bool = False) -> Iterator[dict]:
    rng = np.random.default_rng(seed)
    group = np.resize(np.arange(len(GROUPS)), rows)
    rng.shuffle(group)
    x = rng.uniform(-2, 2, rows)
    if tail:
        x = rng.choice([-1.0, 1.0], rows) * rng.uniform(1.8, 2.0, rows)
    y = SLOPES[group] * x + OFFSETS[group] + rng.normal(0, 0.1, rows)
    if shuffled:
        y = y[rng.permutation(rows)]
    for index in range(rows):
        yield {"group": GROUPS[group[index]], "x": float(x[index]), "y": float(y[index])}


def build() -> rf.Model:
    model = rf.Model.xs(
        batch_size=128,
        group=rf.Enum(values=GROUPS, p_unavailable=0.0),
        x=rf.Number,
        y=rf.Number(mask=True, objective="mse"),
    )
    model.optimizer = rf.adamw(learning_rate=0.002)
    return model


def predict(model: rf.Model, rows: list[dict]) -> np.ndarray:
    visible = [{"group": row["group"], "x": row["x"]} for row in rows]
    result = model.predict(pa.Table.from_pylist(visible))["predictions"].to_pylist()
    return np.asarray([row["y"]["content"] for row in result], dtype=float)


def auc(normal: np.ndarray, anomalous: np.ndarray) -> float:
    """Probability that an anomaly scores above a normal row, with half credit for ties."""
    ordered = np.sort(normal)
    left = np.searchsorted(ordered, anomalous, side="left")
    right = np.searchsorted(ordered, anomalous, side="right")
    return float(np.mean((left + right) / (2 * len(normal))))


def run(seed: int, steps: int | None, accelerator: str) -> tuple[dict, dict]:
    stream = seed * 100
    validation = list(records(rows=2048, seed=stream + 2))
    test = list(records(rows=TEST_ROWS, seed=stream + 3))
    anomalous = list(records(rows=TEST_ROWS, seed=stream + 3, shuffled=True))
    tail = list(records(rows=TEST_ROWS, seed=stream + 4, tail=True))
    actual = np.asarray([row["y"] for row in test])
    broken = np.asarray([row["y"] for row in anomalous])
    train_mean = float(np.mean([row["y"] for row in records(rows=TRAIN_ROWS, seed=stream + 1)]))
    baseline_rmse = float(np.sqrt(np.mean((actual - train_mean) ** 2)))
    marginal_auc = auc(np.abs(actual - train_mean), np.abs(broken - train_mean))
    metrics = {"train_rows": TRAIN_ROWS, "test_rows_per_condition": TEST_ROWS, "marginal_auc": marginal_auc}
    checks = {
        "Anomalies preserve the exact target marginal": bool(np.array_equal(np.sort(actual), np.sort(broken))),
        "Anomalies preserve visible contexts": all(
            (a["group"], a["x"]) == (b["group"], b["x"]) for a, b in zip(test, anomalous, strict=True)
        ),
        "Marginal deviation baseline has exactly chance AUROC": abs(marginal_auc - 0.5) < 1e-12,
    }
    for shuffled in (False, True):
        lit.seed_everything(seed, workers=True)
        model = build()
        data = rf.SyntheticDataModule(
            model=model,
            train=partial(records, rows=TRAIN_ROWS, seed=stream + 1, shuffled=shuffled),
            validate=partial(records, rows=2048, seed=stream + 2, shuffled=shuffled),
            seed=seed,
        )
        trainer = lit.Trainer(
            accelerator=accelerator,
            devices=1,
            max_epochs=-1,
            max_steps=BUDGET if steps is None else min(steps, BUDGET),
            deterministic=True,
            logger=False,
            enable_checkpointing=False,
            enable_progress_bar=False,
            enable_model_summary=False,
            num_sanity_val_steps=0,
        )
        trainer.fit(model, datamodule=data)
        model.eval()
        valid_error = np.abs(predict(model, validation) - [row["y"] for row in validation])
        threshold = float(np.quantile(valid_error, 0.95))
        prediction = predict(model, test)
        clean_score = np.abs(prediction - actual)
        broken_score = np.abs(prediction - broken)
        tail_score = np.abs(predict(model, tail) - [row["y"] for row in tail])
        key = "shuffled_training" if shuffled else "informative"
        result = {
            "steps": trainer.global_step,
            "validation_threshold": threshold,
            "rmse": float(np.sqrt(np.mean(clean_score**2))),
            "baseline_rmse": baseline_rmse,
            "normalized_rmse": float(np.sqrt(np.mean(clean_score**2)) / baseline_rmse),
            "anomaly_auc": auc(clean_score, broken_score),
            "detection_recall": float(np.mean(broken_score > threshold)),
            "normal_false_positive_rate": float(np.mean(clean_score > threshold)),
            "tail_false_positive_rate": float(np.mean(tail_score > threshold)),
        }
        metrics[key] = result
        checks[f"{key}: every measurement is finite"] = bool(np.isfinite(list(result.values())).all())
        if shuffled:
            checks["Shuffled training anomaly AUROC stays below 0.65"] = result["anomaly_auc"] <= 0.65
        else:
            checks["Informative reconstruction nRMSE below 0.20"] = result["normalized_rmse"] < 0.20
            checks["Contextual anomaly AUROC reaches 0.90"] = result["anomaly_auc"] >= 0.90
            checks["Anomaly recall reaches 0.75"] = result["detection_recall"] >= 0.75
            checks["Normal false positive rate at most 0.10"] = result["normal_false_positive_rate"] <= 0.10
            checks["Rare valid tail false positive rate at most 0.15"] = result["tail_false_positive_rate"] <= 0.15
    return metrics, checks


if __name__ == "__main__":
    report(PROOF_ID, run, seed=7601)