Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -551,10 +551,10 @@ byte-identical. Three consequences are worth naming because none is obvious:
* **`trials` does not apply.** A recording holds the points it holds; asking
for ten against a three-sample recording would be seven invented ones or
seven copies.
* **`_PREPARE_INPUTS` is skipped.** The hook exists to drag *generated* inputs
into the physical domain, and recorded ones are already there. It also ships
inside the artifact under test, so running it on a replay would let the
candidate edit the production run's own numbers before being judged on them.
* **`recast_inputs.py` is skipped.** The project's input profile exists to
drag *generated* inputs into the physical domain, and recorded ones are
already there. Running it on a replay would let the production run's own
numbers be edited before the artifact is judged on them.
* **Reference-side `setup` is skipped.** A replayed reference has no state to
set: whatever the run's module state was is folded into what it recorded.
An operator whose `setup` does not match the run's own initialization gets a
Expand Down
11 changes: 11 additions & 0 deletions src/recast/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ class OracleUnavailable(RecastError):
"""The reference could not be materialized. Verdicts must be FAILED, not skipped."""


class InputProfileError(RecastError):
"""The project's ``recast_inputs.py`` shaped inputs the reference does not take.

A shaped draw is the project asserting that these are inputs the source
accepts. When the reference cannot run them -- it raises, or computes a
NaN -- the assertion is what is wrong, not the translation, so the
verification stops here instead of charging the candidate with it or
drawing again. Fix the profile; nothing about the engine is being judged.
"""


class ScannerUnavailable(RecastError):
"""A Scanner or Adjudicator could not run at all. The stage is INCOMPLETE.

Expand Down
8 changes: 7 additions & 1 deletion src/recast/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from typing import Any

from recast import OUTPUT_DIRNAME, WORKSPACE_DIRNAME, __version__
from recast.errors import ConfigError, RecastError, ScannerUnavailable
from recast.errors import ConfigError, InputProfileError, RecastError, ScannerUnavailable
from recast.model import (
Candidate,
Disclosure,
Expand Down Expand Up @@ -1094,6 +1094,12 @@ def _walk_stage(
verdict = verifier.verify(
unit, unit_run.candidate, unit_run.oracle, workspace, executor, config
)
except InputProfileError:
# The project's recast_inputs.py shaped a draw the reference
# refused. That is the profile's fault, not this unit's: handed
# down as a unit failure it would read as a translation defect
# and send a repair agent after the engine. The walk ends on it.
raise
except Exception as error:
# Fail closed: a verifier that crashed has not compared anything,
# and the unit fails on that rather than the walk ending here.
Expand Down
266 changes: 218 additions & 48 deletions src/recast/verify/bitexact.py

Large diffs are not rendered by default.

190 changes: 190 additions & 0 deletions tests/test_bitexact_draws.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,193 @@ def test_a_draw_that_needs_no_redrawing_is_the_one_the_seed_names(tmp_path: Path
verdict = judge(tmp_path, plain, SimpleNamespace(w_probe=lambda x: x * 2.0))
assert verdict.confidence is Confidence.BIT_EXACT, verdict.detail
assert verdict.metrics["subprograms"]["probe"]["redrawn"] == 0


# -- the project's input profile ----------------------------------------------


def profile(tmp_path: Path, body: str) -> Path:
"""Write ``recast_inputs.py`` at a project root and return that root."""
root = tmp_path / "project"
root.mkdir(exist_ok=True)
(root / "recast_inputs.py").write_text(body)
return root


PACKED_PROFILE = """\
import numpy as np


def prepare(unit, subprogram, inputs, rng):
assert unit == "draw:m" and subprogram == "probe"
n = int(inputs["n"])
lr = n * (n + 1) // 2
inputs["lr"] = np.int32(lr)
inputs["r"] = np.asfortranarray(rng.uniform(-1.0, 1.0, size=lr))
return inputs
"""


def test_a_shaped_draw_is_compared_as_shaped_and_never_redrawn(tmp_path: Path) -> None:
"""The packed workspace that fails by name under the generated rules is
compared as drawn once the project says how ``lr`` follows ``n``: no
redraw, nothing moved, and the trials are recorded as shaped."""
root = profile(tmp_path, PACKED_PROFILE)
verdict = judge(
tmp_path,
PACKED,
SimpleNamespace(w_probe=lambda n, lr, r: float(r[(int(n) * (int(n) + 1)) // 2 - 1])),
root=str(root),
)
assert verdict.confidence is Confidence.BIT_EXACT, verdict.detail
probe = verdict.metrics["subprograms"]["probe"]
assert (probe["redrawn"], probe["reshaped"], probe["shaped"]) == (0, 0, 10)
assert verdict.metrics["input_profile"] == "recast_inputs.py"
assert verdict.metrics["shaped"] == ["probe"]
# The root is a checkout whose cleanliness is checked: reading the
# profile must not drop bytecode into it.
assert sorted(entry.name for entry in root.iterdir()) == ["recast_inputs.py"]


def test_a_candidate_that_refuses_a_shaped_draw_has_failed(tmp_path: Path) -> None:
"""Under the generated rules a translated ERROR STOP is a draw to make
again. Under a profile that fixed ``mode`` to a value the source takes,
the reference answers and the candidate stops: that is the translation
refusing inputs the source accepts, and it fails by name."""
root = profile(
tmp_path,
"import numpy as np\n"
"def prepare(unit, subprogram, inputs, rng):\n"
" inputs['mode'] = np.int32(1)\n"
" return inputs\n",
)
wrong = MODE.replace("if int(mode) not in (1, 2):", "if int(mode) != 2:")
verdict = judge(
tmp_path, wrong, SimpleNamespace(w_probe=lambda mode, x: x * 2.0), root=str(root)
)
assert verdict.confidence is Confidence.FAILED
detail = verdict.detail or ""
assert "probe: candidate raised on shaped inputs the reference took: SystemExit" in detail
assert "redrawn" not in detail


def test_shaped_inputs_the_reference_refuses_are_the_profiles_fault(tmp_path: Path) -> None:
"""The profile asserts the reference takes the draw, so the reference is
called first. When it refuses, nothing about the candidate is being
judged: the gate stops with the profile, subprogram and trial named."""
from recast.errors import InputProfileError

root = profile(
tmp_path,
"import numpy as np\n"
"def prepare(unit, subprogram, inputs, rng):\n"
" inputs['mode'] = np.int32(7)\n"
" return inputs\n",
)

def w_probe(mode: Any, x: Any) -> Any:
# Standing in for an ERROR STOP the reference would end the process on.
if int(mode) not in (1, 2):
raise ValueError("invalid mode in probe")
return x * 2.0

with pytest.raises(InputProfileError) as caught:
judge(tmp_path, MODE, SimpleNamespace(w_probe=w_probe), root=str(root))
message = str(caught.value)
assert message.startswith("recast_inputs.py: prepare('draw:m', 'probe') at trial 0")
assert "the reference does not take: ValueError: invalid mode in probe" in message


def test_a_reference_nan_on_shaped_inputs_is_the_profiles_fault(tmp_path: Path) -> None:
"""Both sides going to NaN is a redraw under the generated rules. A
shaped draw is not drawn again, so a reference NaN on it is the profile
having put the source outside its numeric domain."""
from recast.errors import InputProfileError

root = profile(
tmp_path,
"import numpy as np\n"
"def prepare(unit, subprogram, inputs, rng):\n"
" inputs['x'] = np.float64(-4.0)\n"
" return inputs\n",
)

def w_probe(x: Any) -> Any:
with np.errstate(invalid="ignore"):
return np.sqrt(x)

with pytest.raises(InputProfileError, match="the reference produced NaN in y"):
judge(tmp_path, NAN, SimpleNamespace(w_probe=w_probe), root=str(root))


def test_a_profile_that_returns_none_leaves_the_draw_to_the_generated_rules(
tmp_path: Path,
) -> None:
root = profile(
tmp_path,
"def prepare(unit, subprogram, inputs, rng):\n return None\n",
)

def w_probe(mode: Any, x: Any) -> Any:
assert int(mode) in (1, 2), "the reference was called on a refused draw"
return x * 2.0

verdict = judge(tmp_path, MODE, SimpleNamespace(w_probe=w_probe), root=str(root))
assert verdict.confidence is Confidence.BIT_EXACT, verdict.detail
probe = verdict.metrics["subprograms"]["probe"]
assert probe["redrawn"] > 0 and probe["shaped"] == 0
assert verdict.metrics["input_profile"] == "recast_inputs.py"
assert verdict.metrics["shaped"] == []


def test_a_profile_that_edits_in_place_and_returns_none_is_refused(tmp_path: Path) -> None:
"""The profile sees a copy; the only way its work reaches the comparison
is by returning it. Otherwise an edited draw would be judged under the
unshaped rules with nobody told."""
from recast.errors import InputProfileError

root = profile(
tmp_path,
"import numpy as np\n"
"def prepare(unit, subprogram, inputs, rng):\n"
" inputs['mode'] = np.int32(1)\n",
)
with pytest.raises(InputProfileError, match="edited mode in place and returned None"):
judge(tmp_path, MODE, SimpleNamespace(w_probe=lambda mode, x: x * 2.0), root=str(root))


def test_a_profile_that_renames_the_arguments_is_refused(tmp_path: Path) -> None:
from recast.errors import InputProfileError

root = profile(
tmp_path,
"def prepare(unit, subprogram, inputs, rng):\n"
" return {'mode': inputs['mode'], 'xx': inputs['x']}\n",
)
with pytest.raises(InputProfileError, match="missing x; unknown xx"):
judge(tmp_path, MODE, SimpleNamespace(w_probe=lambda mode, x: x * 2.0), root=str(root))


def test_a_profile_that_does_not_import_stops_the_gate(tmp_path: Path) -> None:
"""A tree with a profile that cannot be read is not a tree without one."""
from recast.errors import InputProfileError

root = profile(tmp_path, "def prepare(unit, subprogram, inputs, rng)\n return None\n")
with pytest.raises(InputProfileError, match=r"recast_inputs\.py does not import: SyntaxError"):
judge(tmp_path, MODE, SimpleNamespace(w_probe=lambda mode, x: x * 2.0), root=str(root))
root = profile(tmp_path, "PREPARE = None\n")
with pytest.raises(InputProfileError, match="defines no callable prepare"):
judge(tmp_path, MODE, SimpleNamespace(w_probe=lambda mode, x: x * 2.0), root=str(root))


def test_a_root_without_a_profile_is_the_generated_path(tmp_path: Path) -> None:
root = tmp_path / "bare"
root.mkdir()
verdict = judge(
tmp_path,
NAN.replace("return np.sqrt(x)", "return x * 2.0"),
SimpleNamespace(w_probe=lambda x: x * 2.0),
root=str(root),
)
assert verdict.confidence is Confidence.BIT_EXACT, verdict.detail
assert verdict.metrics["input_profile"] is None
28 changes: 17 additions & 11 deletions tests/test_dump_replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,22 +279,27 @@ def test_an_oracle_that_supplies_no_samples_fails_closed(tmp_path: Path) -> None
assert "handed over no samples" in verdict.detail


def test_a_candidate_hook_cannot_edit_the_recorded_inputs(tmp_path: Path) -> None:
"""``_PREPARE_INPUTS`` shapes *generated* inputs and must not touch these.
def test_the_project_profile_cannot_edit_the_recorded_inputs(tmp_path: Path) -> None:
"""``recast_inputs.py`` shapes *generated* inputs and must not touch these.

The hook ships inside the artifact under test. Letting it rewrite the
production run's own numbers before the artifact is judged on them would
let the candidate choose its own exam.
A recording's inputs are the production run's own numbers. Letting
anything rewrite them before the artifact is judged on them would let
the exam be rewritten, so a replay never loads the profile -- not even
one that would refuse to load.
"""
path = tmp_path / "toy_numpy.py"
path.write_text(
"import numpy as np\n"
f"_SIGNATURES = {SIGNATURES!r}\n"
"def _PREPARE_INPUTS(name, inputs, rng):\n"
" inputs['x'] = inputs['x'] * 0.0\n"
"def scale_by_two(n, x):\n"
" return x * 2.0\n"
)
root = tmp_path / "project"
root.mkdir()
(root / "recast_inputs.py").write_text(
"def prepare(unit, subprogram, inputs, rng):\n"
" raise AssertionError('the profile ran on a replay')\n"
)
candidate = Candidate(
unit="toy",
transform="translate.numpy",
Expand All @@ -308,10 +313,11 @@ def test_a_candidate_hook_cannot_edit_the_recorded_inputs(tmp_path: Path) -> Non
)
from recast.verify.bitexact import BitexactVerifier

verdict = BitexactVerifier().verify(_unit(), candidate, ref, tmp_path, _executor(), {})
# If the hook had run, x would be zeros and the recording's 2/4/6 would
# not be reproduced.
assert verdict.confidence is Confidence.BIT_EXACT
verdict = BitexactVerifier().verify(
_unit(), candidate, ref, tmp_path, _executor(), {"root": str(root)}
)
assert verdict.confidence is Confidence.BIT_EXACT, verdict.detail
assert verdict.metrics["input_profile"] is None


# -- the example, end to end --------------------------------------------------
Expand Down
32 changes: 21 additions & 11 deletions tests/test_f2py_oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -1321,12 +1321,13 @@ def test_wrappers_serve_a_file_of_bare_subprograms() -> None:
assert "real(8), intent(inout) :: t(pcols, pver)" in text


def test_the_gate_lets_a_candidate_shape_its_own_inputs(tmp_path: Path) -> None:
def test_the_project_profile_shapes_the_generated_inputs(tmp_path: Path) -> None:
"""Per-name ranges cannot express structure -- a monotone pressure
column, a consistent thickness field. A candidate may carry
``_PREPARE_INPUTS`` the way it carries ``_SIGNATURES``; both sides then
receive the same shaped arrays, so it chooses the sampled region without
touching the verdict."""
column, a consistent thickness field. The project carries a
``recast_inputs.py`` at its root, and its ``prepare`` shapes every
generated draw before both sides receive it, so it chooses the sampled
region without touching the verdict -- and the candidate, which is the
thing under judgement, has no say in it."""
import numpy as np

module = tmp_path / "candidate"
Expand All @@ -1350,15 +1351,21 @@ def test_the_gate_lets_a_candidate_shape_its_own_inputs(tmp_path: Path) -> None:
SEEN = []


def _PREPARE_INPUTS(name, inputs, rng):
inputs["x"][:] = 2.0 # every trial sees the same shaped input


def step(x):
SEEN.append(float(x[0]))
return np.asarray(x) * 3.0
"""
)
root = tmp_path / "project"
root.mkdir()
(root / "recast_inputs.py").write_text(
"import numpy as np\n"
"\n"
"def prepare(unit, subprogram, inputs, rng):\n"
" assert unit == 'fortran:shaped' and subprogram == 'step'\n"
" inputs['x'] = np.full_like(inputs['x'], 2.0) # every trial sees the same input\n"
" return inputs\n"
)

class Truth:
@staticmethod
Expand All @@ -1382,15 +1389,18 @@ def w_step(x):
ref,
tmp_path / "ws",
LocalExecutor(),
{"trials": 3, "dims": {"n": 4}, "ranges": {"x": (100.0, 200.0)}},
{"root": str(root), "trials": 3, "dims": {"n": 4}, "ranges": {"x": (100.0, 200.0)}},
)
assert verdict.confidence is Confidence.BIT_EXACT
assert verdict.metrics["input_profile"] == "recast_inputs.py"
assert verdict.metrics["shaped"] == ["step"]
assert verdict.metrics["subprograms"]["step"]["shaped"] == 3
staged = tmp_path / "ws" / "candidate"
sys.path.insert(0, str(staged))
try:
import shaped_numpy

# The hook ran: every trial saw 2.0, not a value from the range.
# The profile ran: every trial saw 2.0, not a value from the range.
assert shaped_numpy.SEEN and all(v == 2.0 for v in shaped_numpy.SEEN)
finally:
sys.path.remove(str(staged))
Expand Down
Loading