Can it infer a new rule from examples in the record?

Learning from context
Each record supplies four examples of a fresh straight-line rule and asks for the answer at a new input.

Example

examples:
  - {x: -1.0, y: -0.5}
  - {x: -0.3, y: 0.2}
  - {x: 0.3, y: 0.8}
  - {x: 1.0, y: 1.5}
query: 0.6
answer: 1.1

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

Model tree with task, examples, x, y, query, answer. Amber cards are hidden prediction targets. Repeated collections keep their fields together.

Model tree with task, examples, x, y, query, answer. Amber cards are hidden prediction targets. Repeated collections keep their fields together.

Figure 1: Amber cards are hidden prediction targets. Repeated collections keep their fields together.

Comparison

Swap the examples between records, remove them, and compare with fitting a line directly. Test a region of rules excluded from training.

Result

P072 · Latest full run · Partial

Seed 7203 on cpu: 17 of 17 test checks met. The broader claim remains incomplete. History: 1 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
Familiar rules 6.6% of baseline error
Excluded rules 36.2% of baseline error
Excluded rules, direct line fit 1.1% of baseline error

Baseline error compares prediction error with a constant guess. 100% matches that guess; lower is better. Details specify the baseline and root-mean-square error calculation.

Examples help in familiar regions, but only one of three seeds passes every check. Reliable transfer to the excluded rule region remains unestablished.

Experiment details and code

Latest full run

Seed 7203, cpu, recorded 2026-09-22T17:12:08.778590+00:00. Outcome: met.

Source fingerprint: 3479d6c91fb1e812c5a59c02361ecc8b40c9d2e743116b5f75b3dab3756c2107. Python 3.12.10; Torch 2.12.0.

Measurement Value
train_rows 4096
test_rows_per_split 2048
training_mean -0.218802
context steps: 600; parameters: 321734; familiar: rmse: 0.0575096; baseline_rmse: 0.877387; normalized_rmse: 0.0655464; swapped_context: rmse: 1.21725; baseline_rmse: 0.877387; normalized_rmse: 1.38736; least_squares: rmse: 0.0116467; baseline_rmse: 0.877387; normalized_rmse: 0.0132743; withheld: rmse: 0.386606; baseline_rmse: 1.06695; normalized_rmse: 0.362347; swapped_context: rmse: 0.509401; baseline_rmse: 1.06695; normalized_rmse: 0.477437; least_squares: rmse: 0.0118618; baseline_rmse: 1.06695; normalized_rmse: 0.0111175
query_only steps: 600; parameters: 217542; familiar: rmse: 0.868118; baseline_rmse: 0.877387; normalized_rmse: 0.989436; withheld: rmse: 1.14333; baseline_rmse: 1.06695; normalized_rmse: 1.07158
Behavioral checks
Behavioral check Outcome
context: familiar predictions are finite Met
familiar: visible-example least-squares RMSE below 0.03 Met
familiar: swapped predictions are finite Met
context: withheld predictions are finite Met
withheld: visible-example least-squares RMSE below 0.03 Met
withheld: swapped predictions are finite Met
context: prediction leaves every weight unchanged Met
query_only: familiar predictions are finite Met
query_only: withheld predictions are finite Met
query_only: prediction leaves every weight unchanged Met
familiar: context normalized RMSE below 0.35 Met
familiar: context more than halves query-only RMSE Met
withheld: context normalized RMSE below 0.40 Met
withheld: context more than halves query-only RMSE Met
Familiar context swapping exceeds 0.80 normalized RMSE Met
Familiar context swapping more than doubles RMSE Met
Familiar query-only normalized RMSE exceeds 0.75 Met

Full run history

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

Seed Outcome Recorded Code fingerprint
7201 not met 2026-09-22T17:11:17.254829+00:00 672cd5cebdc24bf553b389191e00d9e8079cf224f799413a63e716c9e3e10805
7202 not met 2026-09-22T17:11:43.725586+00:00 672cd5cebdc24bf553b389191e00d9e8079cf224f799413a63e716c9e3e10805
7203 met 2026-09-22T17:12:08.778590+00:00 672cd5cebdc24bf553b389191e00d9e8079cf224f799413a63e716c9e3e10805

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 P072

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

PYTHONPATH=proofs python proofs/composition/context_examples.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.

Rules, observations, and splits

Independently for each record, sample slope magnitude Uniform(0.75, 1.75) and intercept magnitude Uniform(0.25, 1). Training draws the three sign combinations (-,-), (-,+), and (+,-). The entirely withheld quadrant (+,+) is used only for testing. Thus each sign and each magnitude is familiar, but their positive/positive combination is absent from training and validation. Four example x values are jittered anchors around -1, -1/3, 1/3, and 1; their y values equal slope*x + intercept + Normal(0, 0.01). Query x is Uniform(-0.8, 0.8), independently drawn; the hidden answer uses the same rule and independent Normal(0, 0.01) noise. Neither slope nor intercept is included in the observation. The query lies between examples.

Independent streams supply 4,096 training rules, 512 validation rules, 2,048 familiar-quadrant test rules, and 2,048 withheld-quadrant test rules. No task or example is reused across splits. A continuous rule family avoids a finite task lookup table, while the quadrant split supplies a concrete extrapolation of task combinations rather than extrapolation of query range.

Training and predeclared gates

The context model and query-only model each receive 600 xs AdamW updates at learning rate 0.002, batch size 128, with identical observations and seeds. Removing the example branch reduces the control’s parameters; it intentionally measures what the query alone permits, rather than matching unused capacity. The final update is evaluated once without checkpoint selection or adaptation.

Normalize RMSE by the training-mean predictor evaluated on each test split. Require context normalized RMSE below 0.35 on familiar quadrants and 0.40 on the withheld quadrant. On familiar quadrants, context swapping must exceed 0.80 and double the intact error, and the query-only model must exceed 0.75. Both splits must improve on query-only error by at least a factor of two. Least-squares RMSE must be below 0.03 in source units on both splits, confirming observability independently of neural optimization. Swapping within the withheld quadrant remains diagnostic: its slope and intercept signs are constant, so such a swap leaves useful information intact.

Snapshot every model parameter before prediction and require exact equality afterward. This checks the limited no-weight-update claim, alongside finite outputs. It does not describe a training-free system: meta-training on many affine tasks is what may produce this behavior.

Complete experiment code

Code
"""P072: infer a fresh bounded affine rule from noisy examples without weight updates."""

from collections.abc import Iterator
from functools import partial

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

import relflow as rf

PROOF_ID = "P072"
BUDGET = 600
TRAIN_ROWS = 4096
TEST_ROWS = 2048
QUADRANTS = ((-1, -1), (-1, 1), (1, -1))


def records(*, rows: int, seed: int, withheld: bool = False) -> Iterator[dict]:
    rng = np.random.default_rng(seed)
    signs = ((1, 1),) if withheld else QUADRANTS
    quadrants = np.resize(np.arange(len(signs)), rows)
    rng.shuffle(quadrants)
    for quadrant in quadrants:
        slope_sign, intercept_sign = signs[quadrant]
        slope = slope_sign * rng.uniform(0.75, 1.75)
        intercept = intercept_sign * rng.uniform(0.25, 1.0)
        example_x = np.linspace(-1.0, 1.0, 4) + rng.uniform(-0.04, 0.04, 4)
        example_y = slope * example_x + intercept + rng.normal(0.0, 0.01, 4)
        order = rng.permutation(4)
        query = rng.uniform(-0.8, 0.8)
        yield {
            "examples": [{"x": float(example_x[i]), "y": float(example_y[i])} for i in order],
            "query": float(query),
            "answer": float(slope * query + intercept + rng.normal(0.0, 0.01)),
        }


def build(*, context: bool = True) -> rf.Model:
    fields = {"query": rf.Number, "answer": rf.Number(mask=True, objective="mse")}
    if context:
        fields["examples"] = rf.Branch(length=4, reduction=None, x=rf.Number, y=rf.Number)
    model = rf.Model.xs(
        batch_size=128,
        fields=fields,
    )
    model.optimizer = rf.adamw(learning_rate=0.002)
    return model


def predict(model: rf.Model, rows: list[dict]) -> np.ndarray:
    visible = [{key: value for key, value in row.items() if key != "answer"} for row in rows]
    output = model.predict(pa.Table.from_pylist(visible))["predictions"].to_pylist()
    return np.asarray([row["answer"]["content"] for row in output])


def swap(rows: list[dict], seed: int) -> list[dict]:
    """Exchange complete example sets; preserve each query and its original target."""
    order = np.random.default_rng(seed).permutation(len(rows))
    return [{**row, "examples": rows[index]["examples"]} for row, index in zip(rows, order, strict=True)]


def least_squares(rows: list[dict]) -> np.ndarray:
    """Fit each rule only from its visible examples, without generator parameters."""
    x = np.asarray([[example["x"] for example in row["examples"]] for row in rows])
    y = np.asarray([[example["y"] for example in row["examples"]] for row in rows])
    centered = x - x.mean(axis=1, keepdims=True)
    slope = (centered * y).sum(axis=1) / (centered**2).sum(axis=1)
    query = np.asarray([row["query"] for row in rows])
    return y.mean(axis=1) + slope * (query - x.mean(axis=1))


def score(predicted: np.ndarray, rows: list[dict], training_mean: float) -> dict:
    target = np.asarray([row["answer"] for row in rows])
    rmse = float(np.sqrt(np.mean((predicted - target) ** 2)))
    baseline = float(np.sqrt(np.mean((training_mean - target) ** 2)))
    return {"rmse": rmse, "baseline_rmse": baseline, "normalized_rmse": rmse / baseline}


def run(seed: int, steps: int | None, accelerator: str) -> tuple[dict, dict]:
    tests = {
        "familiar": list(records(rows=TEST_ROWS, seed=seed * 100 + 3)),
        "withheld": list(records(rows=TEST_ROWS, seed=seed * 100 + 4, withheld=True)),
    }
    training_mean = float(np.mean([row["answer"] for row in records(rows=TRAIN_ROWS, seed=seed * 100 + 1)]))
    metrics = {"train_rows": TRAIN_ROWS, "test_rows_per_split": TEST_ROWS, "training_mean": training_mean}
    checks = {}
    for context in (True, False):
        lit.seed_everything(seed, workers=True)
        model = build(context=context)
        data = rf.SyntheticDataModule(
            model=model,
            train=partial(records, rows=TRAIN_ROWS, seed=seed * 100 + 1),
            validate=partial(records, rows=512, seed=seed * 100 + 2),
            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()
        snapshot = [parameter.detach().clone() for parameter in model.parameters()]
        arm = "context" if context else "query_only"
        measured = {"steps": trainer.global_step, "parameters": sum(value.numel() for value in snapshot)}
        for name, rows in tests.items():
            output = predict(model, rows)
            measured[name] = score(output, rows, training_mean)
            checks[f"{arm}: {name} predictions are finite"] = bool(np.isfinite(output).all())
            if context:
                control = predict(model, swap(rows, seed=seed * 100 + 5))
                measured[name]["swapped_context"] = score(control, rows, training_mean)
                oracle = score(least_squares(rows), rows, training_mean)
                measured[name]["least_squares"] = oracle
                checks[f"{name}: visible-example least-squares RMSE below 0.03"] = oracle["rmse"] < 0.03
                checks[f"{name}: swapped predictions are finite"] = bool(np.isfinite(control).all())
        checks[f"{arm}: prediction leaves every weight unchanged"] = all(
            torch.equal(before, after) for before, after in zip(snapshot, model.parameters(), strict=True)
        )
        metrics[arm] = measured
    for name, gate in (("familiar", 0.35), ("withheld", 0.40)):
        context_error = metrics["context"][name]["normalized_rmse"]
        control_error = metrics["query_only"][name]["normalized_rmse"]
        checks[f"{name}: context normalized RMSE below {gate:.2f}"] = context_error < gate
        checks[f"{name}: context more than halves query-only RMSE"] = context_error < control_error / 2
    familiar = metrics["context"]["familiar"]
    swapped = familiar["swapped_context"]["normalized_rmse"]
    checks["Familiar context swapping exceeds 0.80 normalized RMSE"] = swapped > 0.80
    checks["Familiar context swapping more than doubles RMSE"] = swapped > 2 * familiar["normalized_rmse"]
    checks["Familiar query-only normalized RMSE exceeds 0.75"] = (
        metrics["query_only"]["familiar"]["normalized_rmse"] > 0.75
    )
    return metrics, checks


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