diff --git a/AGENTS.md b/AGENTS.md index 7a55fb3..50e837e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -484,6 +484,13 @@ class MetaCognition: Date: 2025-09-12 Test Result: pytest tests/test_recolor_fix.py passed Notes: Standardised 'mapping' parameter across heuristics; episodic loader normalises keys + +[X] Step 4.3 UPDATE2 - Translate parameter mismatch fixed preventing training warnings + Date: 2025-09-13 + Test Result: pytest tests/test_translate_fix.py passed; python tools/train_guidance_on_arc.py --epochs 1 + Notes: Canonicalised 'fill' parameter for translate; legacy 'fill_value' still accepted + + ``` --- diff --git a/arc_solver/dsl.py b/arc_solver/dsl.py index c6b6e08..041def1 100644 --- a/arc_solver/dsl.py +++ b/arc_solver/dsl.py @@ -60,7 +60,7 @@ def op_transpose(a: Array) -> Array: return transpose_grid(a) -def op_translate(a: Array, dy: int, dx: int, fill: Optional[int] = None) -> Array: +def op_translate(a: Array, dy: int, dx: int, fill: Optional[int] = None, *, fill_value: Optional[int] = None) -> Array: """Translate the grid by ``(dy, dx)`` filling uncovered cells. Parameters @@ -69,11 +69,13 @@ def op_translate(a: Array, dy: int, dx: int, fill: Optional[int] = None) -> Arra Input grid. dy, dx: Translation offsets. Positive values move content down/right. - fill: - Optional fill value for uncovered cells. If ``None`` the background - colour of ``a`` is used. + fill, fill_value: + Optional fill value for uncovered cells. ``fill_value`` is an alias for + backward compatibility. When both are ``None`` the background colour of + ``a`` is used. """ - fill_val = 0 if fill is None else fill + chosen = fill if fill is not None else fill_value + fill_val = 0 if chosen is None else chosen return translate_grid(a, dy, dx, fill=fill_val) @@ -114,6 +116,20 @@ def op_pad(a: Array, out_h: int, out_w: int) -> Array: _sem_cache: Dict[Tuple[bytes, str, Tuple[Tuple[str, Any], ...]], Array] = {} +def _canonical_params(name: str, params: Dict[str, Any]) -> Dict[str, Any]: + """Return a copy of ``params`` with legacy aliases normalised.""" + if name == "recolor" and "mapping" not in params and "color_map" in params: + new_params = dict(params) + new_params["mapping"] = new_params.pop("color_map") + return new_params + if name == "translate" and "fill" not in params and "fill_value" in params: + new_params = dict(params) + new_params["fill"] = new_params.pop("fill_value") + return new_params + return params + + + def _norm_params(params: Dict[str, Any]) -> Tuple[Tuple[str, Any], ...]: """Normalise parameters to a hashable tuple.""" items: List[Tuple[str, Any]] = [] @@ -127,6 +143,7 @@ def _norm_params(params: Dict[str, Any]) -> Tuple[Tuple[str, Any], ...]: def apply_op(a: Array, name: str, params: Dict[str, Any]) -> Array: """Apply a primitive operation with semantic caching.""" + params = _canonical_params(name, params) key = (a.tobytes(), name, _norm_params(params)) cached = _sem_cache.get(key) if cached is not None: diff --git a/arc_solver/dsl_complete.py b/arc_solver/dsl_complete.py index 52b072b..ecd869d 100644 --- a/arc_solver/dsl_complete.py +++ b/arc_solver/dsl_complete.py @@ -39,15 +39,20 @@ def transpose(grid: Array) -> Array: return grid.T -def translate(grid: Array, dx: int = 0, dy: int = 0, fill_value: int = 0) -> Array: - """Translate grid by (dx, dy) with wraparound or filling.""" +def translate(grid: Array, dy: int = 0, dx: int = 0, fill: int = 0, *, fill_value: int | None = None) -> Array: + """Translate grid by (dy, dx) with wraparound or filling. + + ``fill_value`` is accepted for backward compatibility. + """ + if fill_value is not None and fill == 0: + fill = fill_value H, W = grid.shape - result = np.full_like(grid, fill_value) + result = np.full_like(grid, fill) # Source bounds src_y_start = max(0, -dy) src_y_end = min(H, H - dy) - src_x_start = max(0, -dx) + src_x_start = max(0, -dx) src_x_end = min(W, W - dx) # Destination bounds @@ -693,7 +698,7 @@ def get_operation_signatures() -> Dict[str, List[str]]: 'rotate': ['k'], 'flip': ['axis'], 'transpose': [], - 'translate': ['dx', 'dy', 'fill_value'], + 'translate': ['dy', 'dx', 'fill'], 'crop': ['top', 'bottom', 'left', 'right'], 'pad': ['top', 'bottom', 'left', 'right', 'fill_value'], 'resize': ['new_height', 'new_width', 'method'], diff --git a/arc_solver/heuristics_complete.py b/arc_solver/heuristics_complete.py index b4c3c95..d8964fe 100644 --- a/arc_solver/heuristics_complete.py +++ b/arc_solver/heuristics_complete.py @@ -104,7 +104,7 @@ def detect_basic_transformations(inp: Array, out: Array) -> List[List[Tuple[str, translated = translate_safe(inp, dx, dy) if np.array_equal(translated, out): - programs.append([('translate', {'dx': dx, 'dy': dy, 'fill_value': 0})]) + programs.append([('translate', {'dy': dy, 'dx': dx, 'fill': 0})]) return programs @@ -497,8 +497,8 @@ def infer_color_mapping(inp: Array, out: Array) -> Optional[Dict[int, int]]: for i in range(inp.shape[0]): for j in range(inp.shape[1]): - inp_color = inp[i, j] - out_color = out[i, j] + inp_color = int(inp[i, j]) + out_color = int(out[i, j]) if inp_color in color_map: if color_map[inp_color] != out_color: diff --git a/tests/test_translate_fix.py b/tests/test_translate_fix.py new file mode 100644 index 0000000..a1dc69d --- /dev/null +++ b/tests/test_translate_fix.py @@ -0,0 +1,21 @@ +from pathlib import Path +import sys + +import numpy as np + +sys.path.append(str(Path(__file__).resolve().parents[1])) +from arc_solver.dsl import apply_program +from arc_solver.heuristics_complete import detect_basic_transformations + + +def test_translate_fill_value_alias_and_detection(): + inp = np.array([[1, 0], [0, 0]]) + expected = np.array([[0, 0], [0, 1]]) + + # legacy alias still works + legacy_prog = [("translate", {"dy": 1, "dx": 1, "fill_value": 0})] + assert np.array_equal(apply_program(inp, legacy_prog), expected) + + # heuristics now emit canonical parameter name + detected = detect_basic_transformations(inp, expected) + assert [("translate", {"dy": 1, "dx": 1, "fill": 0})] in detected