diff --git a/src/phase_z2_pipeline.py b/src/phase_z2_pipeline.py index 5bc031b..d0b2205 100644 --- a/src/phase_z2_pipeline.py +++ b/src/phase_z2_pipeline.py @@ -953,6 +953,56 @@ def compute_zone_layout_cols(zones_data: list[dict], } +def _aggregate_zone_signals_per_track( + preset: dict, + zones_data: list[dict], +) -> tuple[list[dict], list[dict]]: + """Build per-row + per-col virtual zones for 2-D dynamic dispatch. + + Each virtual zone aggregates content_weight.score (max) and + min_height_px (max) across single-span zones on that track + (occupied_rows == {r} for rows, occupied_cols == {c} for cols). + Falls back to all-span zones (touching every track on the axis) + when a track has no single-span zone. + """ + rows_grid, _ = _parse_css_areas(preset["css_areas"]) + R = len(rows_grid) + C = len(rows_grid[0]) + + occupancy: list[tuple[dict, set[int], set[int]]] = [] + for z in zones_data: + pos = z["position"] + occ_rows = {r for r, row in enumerate(rows_grid) if pos in row} + occ_cols = { + c for row in rows_grid for c, tok in enumerate(row) if tok == pos + } + occupancy.append((z, occ_rows, occ_cols)) + + def _track_virtual(idx: int, axis: str) -> dict: + if axis == "row": + single = [z for z, rr, _cc in occupancy if rr == {idx}] + allspan = [z for z, rr, _cc in occupancy if rr == set(range(R))] + else: + single = [z for z, _rr, cc in occupancy if cc == {idx}] + allspan = [z for z, _rr, cc in occupancy if cc == set(range(C))] + candidates = single or allspan + return { + "position": f"_virtual_{axis}_{idx}", + "template_id": f"_virtual_{axis}_{idx}", + "content_weight": { + "score": max(c["content_weight"]["score"] for c in candidates) + }, + "min_height_px": max( + c.get("min_height_px", DEFAULT_ZONE_MIN_HEIGHT_PX) + for c in candidates + ), + } + + row_virtuals = [_track_virtual(r, "row") for r in range(R)] + col_virtuals = [_track_virtual(c, "col") for c in range(C)] + return row_virtuals, col_virtuals + + def _compute_per_zone_geometry( layout_css: dict, debug_zones: list[dict], @@ -1083,6 +1133,61 @@ def _build_rows_dynamic(preset: dict, zones_data: list[dict], } +def _build_grid_dynamic_2d(preset: dict, zones_data: list[dict], + gap: int = GRID_GAP) -> dict: + """2-D dynamic path — dynamic row heights + dynamic column widths. + + IMP-09 PR 2 (B-4) handler for the five preset families whose topology + is neither pure 'rows' nor pure 'cols': + - T (top-1-bottom-2) + - inverted-T (top-2-bottom-1) + - side-T-left (left-1-right-2) + - side-T-right (left-2-right-1) + - 2x2 (grid-2x2) + + Strategy: + 1) _aggregate_zone_signals_per_track builds R per-row + C per-col + virtual zones (max content_weight.score + max min_height_px of + single-span zones, falling back to all-span zones). + 2) Row virtuals → compute_zone_layout → heights_px (R). + 3) Col virtuals → compute_zone_layout_cols → widths_px (C). + 4) Assemble layout_css dict with computation='2d_dynamic_aggregated' + and dynamic_rows=True, dynamic_cols=True. + + raw_zone_layout carries both solver outputs + the virtual zone lists + so step08 trace can explain the per-track aggregation. + """ + row_virtuals, col_virtuals = _aggregate_zone_signals_per_track( + preset, zones_data + ) + zl_row = compute_zone_layout(row_virtuals, gap=gap) + zl_col = compute_zone_layout_cols(col_virtuals, gap=gap) + + heights_px = zl_row["heights_px"] + widths_px = zl_col["widths_px"] + rows_str = " ".join(f"{h}px" for h in heights_px) + cols_str = " ".join(f"{w}px" for w in widths_px) + + return { + "areas": preset["css_areas"], + "cols": cols_str, + "rows": rows_str, + "heights_px": heights_px, + "widths_px": widths_px, + "ratios": [round(h / SLIDE_BODY_HEIGHT, 3) for h in heights_px], + "width_ratios": [round(w / SLIDE_BODY_WIDTH, 3) for w in widths_px], + "computation": "2d_dynamic_aggregated", + "dynamic_rows": True, + "dynamic_cols": True, + "raw_zone_layout": { + "row_layout": zl_row, + "col_layout": zl_col, + "row_virtuals": row_virtuals, + "col_virtuals": col_virtuals, + }, + } + + def _build_cols_dynamic(preset: dict, zones_data: list[dict], gap: int = GRID_GAP) -> dict: """vertical-2 path — dynamic column widths, static fr row heights. @@ -1113,6 +1218,105 @@ def _build_cols_dynamic(preset: dict, zones_data: list[dict], } +def _override_to_grid_tracks( + preset: dict, + zones_data: list[dict], + override_zone_geometries: dict[str, dict], + gap: int = GRID_GAP, +) -> dict: + """2-D override path — derive heights_px (R) + widths_px (C) from + user-supplied zone_id -> {x, y, w, h} (0~1 within slide-body). + + IMP-09 PR 2 (B-4) override handler for the five preset families + whose topology is neither pure 'rows' nor pure 'cols': + - T (top-1-bottom-2) + - inverted-T (top-2-bottom-1) + - side-T-left (left-1-right-2) + - side-T-right (left-2-right-1) + - 2x2 (grid-2x2) + + Strategy: + 1) Parse css_areas into R x C grid. + 2) For each row r: aggregate h via max over single-row zones + (occupied_rows == {r}); fallback to all-span zones; else 0.0. + 3) For each col c: same with w. + 4) Normalize per-axis (divide by total) and multiply by avail_*, + absorbing rounding diff into the last element. + 5) If total_h or total_w == 0 (degenerate / empty override), + fall back to _build_grid_dynamic_2d default path. + """ + rows_grid, _ = _parse_css_areas(preset["css_areas"]) + R = len(rows_grid) + C = len(rows_grid[0]) + + occupancy: list[tuple[dict, set[int], set[int]]] = [] + for z in zones_data: + pos = z["position"] + occ_rows = {r for r, row in enumerate(rows_grid) if pos in row} + occ_cols = { + c for row in rows_grid for c, tok in enumerate(row) if tok == pos + } + occupancy.append((z, occ_rows, occ_cols)) + + def _track_value(idx: int, axis: str) -> float: + if axis == "row": + single = [z for z, rr, _cc in occupancy if rr == {idx}] + allspan = [z for z, rr, _cc in occupancy if rr == set(range(R))] + key = "h" + else: + single = [z for z, _rr, cc in occupancy if cc == {idx}] + allspan = [z for z, _rr, cc in occupancy if cc == set(range(C))] + key = "w" + candidates = single or allspan + vals = [ + float(override_zone_geometries[z["position"]][key]) + for z in candidates + if z["position"] in override_zone_geometries + ] + return max(vals) if vals else 0.0 + + row_values = [_track_value(r, "row") for r in range(R)] + col_values = [_track_value(c, "col") for c in range(C)] + + total_h = sum(row_values) + total_w = sum(col_values) + if total_h == 0 or total_w == 0: + return _build_grid_dynamic_2d(preset, zones_data, gap=gap) + + row_ratios = [v / total_h for v in row_values] + col_ratios = [v / total_w for v in col_values] + + avail_h = SLIDE_BODY_HEIGHT - gap * (R - 1) + avail_w = SLIDE_BODY_WIDTH - gap * (C - 1) + heights_px = [int(round(r * avail_h)) for r in row_ratios] + widths_px = [int(round(r * avail_w)) for r in col_ratios] + diff_h = avail_h - sum(heights_px) + if diff_h != 0 and heights_px: + heights_px[-1] += diff_h + diff_w = avail_w - sum(widths_px) + if diff_w != 0 and widths_px: + widths_px[-1] += diff_w + + rows_str = " ".join(f"{h}px" for h in heights_px) + cols_str = " ".join(f"{w}px" for w in widths_px) + return { + "areas": preset["css_areas"], + "cols": cols_str, + "rows": rows_str, + "heights_px": heights_px, + "widths_px": widths_px, + "ratios": [round(rr, 3) for rr in row_ratios], + "width_ratios": [round(rr, 3) for rr in col_ratios], + "computation": "user_override_geometry", + "dynamic_rows": True, + "dynamic_cols": True, + "raw_zone_layout": { + "override_applied": True, + "source": override_zone_geometries, + }, + } + + # Layout preset → zone position 순서 = LAYOUT_PRESETS[preset]["positions"] 직접 사용. # 이전 ZONE_POSITIONS_BY_PRESET (type-b 등 legacy 명) 는 dead code 로 제거 (2026-04-29). @@ -1202,21 +1406,29 @@ def build_layout_css(layout_preset: str, zones_data: list[dict], "dynamic_cols": True, "raw_zone_layout": {"override_applied": True, "source": override_zone_geometries}, } + elif topology in ("T", "inverted-T", "side-T-left", "side-T-right", "2x2"): + # IMP-09 PR 2 — 2-D override path (T / inverted-T / side-T / 2x2). + # Degenerate inputs (total_h == 0 or total_w == 0) fall back to + # _build_grid_dynamic_2d inside the helper. + return _override_to_grid_tracks( + preset, zones_data, override_zone_geometries, gap=gap + ) else: - # PR 1 lock — warn-and-fallthrough preserved. - # PR 2 promotes this to strict ValueError via _override_to_grid_tracks. + # warn-and-fallthrough preserved for remaining presets (single). + # PR 3 territory. print( f" [override-warning] zone-geometry override 는 layout '{layout_preset}' 미지원 " - f"(현재 horizontal-2 / vertical-2 만). default layout_css 사용.", + f"(현재 horizontal-2 / vertical-2 / 2-D presets 만). default layout_css 사용.", file=sys.stderr, ) - # ── Dynamic branch — topology dispatch (PR 1: rows / cols only) ── + # ── Dynamic branch — topology dispatch ── if topology == "rows": return _build_rows_dynamic(preset, zones_data, gap) if topology == "cols": return _build_cols_dynamic(preset, zones_data, gap) - # PR 2 will dispatch T / inverted-T / side-T-{left,right} / 2x2 here. + if topology in ("T", "inverted-T", "side-T-left", "side-T-right", "2x2"): + return _build_grid_dynamic_2d(preset, zones_data, gap) # PR 3 will dispatch single here. return _build_fr_default(preset) @@ -3383,7 +3595,14 @@ def run_phase_z2_mvp1( dz["ratio"] = geo["zone_height_ratio"] dz["width_px"] = geo["zone_width_px"] dz["width_ratio"] = geo["zone_width_ratio"] - if layout_css["dynamic_rows"]: + if layout_css["dynamic_rows"] and layout_css.get("dynamic_cols"): + print( + f" zones : 2-D heights {layout_css['heights_px']} px, " + f"widths {layout_css['widths_px']} px, " + f"ratios {layout_css['ratios']}, " + f"width_ratios {layout_css['width_ratios']}" + ) + elif layout_css["dynamic_rows"]: print(f" zones : heights {layout_css['heights_px']} px, ratios {layout_css['ratios']}") elif layout_css.get("dynamic_cols"): print(f" zones : widths {layout_css['widths_px']} px, width_ratios {layout_css['width_ratios']}") diff --git a/tests/phase_z2/fixtures/build_layout_css/grid-2x2_default.yaml b/tests/phase_z2/fixtures/build_layout_css/grid-2x2_default.yaml new file mode 100644 index 0000000..7223220 --- /dev/null +++ b/tests/phase_z2/fixtures/build_layout_css/grid-2x2_default.yaml @@ -0,0 +1,43 @@ +input: + layout_preset: grid-2x2 + zones_data: + - position: top-left + template_id: MOCK_top-left + content_weight: + score: 0.25 + min_height_px: 200 + - position: top-right + template_id: MOCK_top-right + content_weight: + score: 0.25 + min_height_px: 200 + - position: bottom-left + template_id: MOCK_bottom-left + content_weight: + score: 0.25 + min_height_px: 200 + - position: bottom-right + template_id: MOCK_bottom-right + content_weight: + score: 0.25 + min_height_px: 200 + override_zone_geometries: null +expected_layout_css: + areas: '"top-left top-right" "bottom-left bottom-right"' + cols: 583px 583px + rows: 286px 285px + heights_px: + - 286 + - 285 + widths_px: + - 583 + - 583 + ratios: + - 0.489 + - 0.487 + width_ratios: + - 0.494 + - 0.494 + computation: 2d_dynamic_aggregated + dynamic_rows: true + dynamic_cols: true diff --git a/tests/phase_z2/fixtures/build_layout_css/grid-2x2_override.yaml b/tests/phase_z2/fixtures/build_layout_css/grid-2x2_override.yaml new file mode 100644 index 0000000..30cd3d4 --- /dev/null +++ b/tests/phase_z2/fixtures/build_layout_css/grid-2x2_override.yaml @@ -0,0 +1,63 @@ +input: + layout_preset: grid-2x2 + zones_data: + - position: top-left + template_id: MOCK_top-left + content_weight: + score: 0.25 + min_height_px: 200 + - position: top-right + template_id: MOCK_top-right + content_weight: + score: 0.25 + min_height_px: 200 + - position: bottom-left + template_id: MOCK_bottom-left + content_weight: + score: 0.25 + min_height_px: 200 + - position: bottom-right + template_id: MOCK_bottom-right + content_weight: + score: 0.25 + min_height_px: 200 + override_zone_geometries: + top-left: + x: 0 + y: 0 + w: 0.55 + h: 0.4 + top-right: + x: 0.55 + y: 0 + w: 0.45 + h: 0.4 + bottom-left: + x: 0 + y: 0.4 + w: 0.55 + h: 0.6 + bottom-right: + x: 0.55 + y: 0.4 + w: 0.45 + h: 0.6 +expected_layout_css: + areas: '"top-left top-right" "bottom-left bottom-right"' + cols: 641px 525px + rows: 228px 343px + heights_px: + - 228 + - 343 + widths_px: + - 641 + - 525 + ratios: + - 0.4 + - 0.6 + width_ratios: + - 0.55 + - 0.45 + computation: user_override_geometry + dynamic_rows: true + dynamic_cols: true diff --git a/tests/phase_z2/fixtures/build_layout_css/left-1-right-2_default.yaml b/tests/phase_z2/fixtures/build_layout_css/left-1-right-2_default.yaml new file mode 100644 index 0000000..e18e447 --- /dev/null +++ b/tests/phase_z2/fixtures/build_layout_css/left-1-right-2_default.yaml @@ -0,0 +1,38 @@ +input: + layout_preset: left-1-right-2 + zones_data: + - position: left + template_id: MOCK_left + content_weight: + score: 0.5 + min_height_px: 200 + - position: right-top + template_id: MOCK_right-top + content_weight: + score: 0.25 + min_height_px: 200 + - position: right-bottom + template_id: MOCK_right-bottom + content_weight: + score: 0.25 + min_height_px: 200 + override_zone_geometries: null +expected_layout_css: + areas: '"left right-top" "left right-bottom"' + cols: 777px 389px + rows: 286px 285px + heights_px: + - 286 + - 285 + widths_px: + - 777 + - 389 + ratios: + - 0.489 + - 0.487 + width_ratios: + - 0.658 + - 0.33 + computation: 2d_dynamic_aggregated + dynamic_rows: true + dynamic_cols: true diff --git a/tests/phase_z2/fixtures/build_layout_css/left-1-right-2_override.yaml b/tests/phase_z2/fixtures/build_layout_css/left-1-right-2_override.yaml new file mode 100644 index 0000000..b247c36 --- /dev/null +++ b/tests/phase_z2/fixtures/build_layout_css/left-1-right-2_override.yaml @@ -0,0 +1,53 @@ +input: + layout_preset: left-1-right-2 + zones_data: + - position: left + template_id: MOCK_left + content_weight: + score: 0.5 + min_height_px: 200 + - position: right-top + template_id: MOCK_right-top + content_weight: + score: 0.25 + min_height_px: 200 + - position: right-bottom + template_id: MOCK_right-bottom + content_weight: + score: 0.25 + min_height_px: 200 + override_zone_geometries: + left: + x: 0 + y: 0 + w: 0.4 + h: 1.0 + right-top: + x: 0.4 + y: 0 + w: 0.6 + h: 0.5 + right-bottom: + x: 0.4 + y: 0.5 + w: 0.6 + h: 0.5 +expected_layout_css: + areas: '"left right-top" "left right-bottom"' + cols: 466px 700px + rows: 286px 285px + heights_px: + - 286 + - 285 + widths_px: + - 466 + - 700 + ratios: + - 0.5 + - 0.5 + width_ratios: + - 0.4 + - 0.6 + computation: user_override_geometry + dynamic_rows: true + dynamic_cols: true diff --git a/tests/phase_z2/fixtures/build_layout_css/left-2-right-1_default.yaml b/tests/phase_z2/fixtures/build_layout_css/left-2-right-1_default.yaml new file mode 100644 index 0000000..90157f8 --- /dev/null +++ b/tests/phase_z2/fixtures/build_layout_css/left-2-right-1_default.yaml @@ -0,0 +1,38 @@ +input: + layout_preset: left-2-right-1 + zones_data: + - position: left-top + template_id: MOCK_left-top + content_weight: + score: 0.25 + min_height_px: 200 + - position: left-bottom + template_id: MOCK_left-bottom + content_weight: + score: 0.25 + min_height_px: 200 + - position: right + template_id: MOCK_right + content_weight: + score: 0.5 + min_height_px: 200 + override_zone_geometries: null +expected_layout_css: + areas: '"left-top right" "left-bottom right"' + cols: 389px 777px + rows: 286px 285px + heights_px: + - 286 + - 285 + widths_px: + - 389 + - 777 + ratios: + - 0.489 + - 0.487 + width_ratios: + - 0.33 + - 0.658 + computation: 2d_dynamic_aggregated + dynamic_rows: true + dynamic_cols: true diff --git a/tests/phase_z2/fixtures/build_layout_css/left-2-right-1_override.yaml b/tests/phase_z2/fixtures/build_layout_css/left-2-right-1_override.yaml new file mode 100644 index 0000000..18e1738 --- /dev/null +++ b/tests/phase_z2/fixtures/build_layout_css/left-2-right-1_override.yaml @@ -0,0 +1,53 @@ +input: + layout_preset: left-2-right-1 + zones_data: + - position: left-top + template_id: MOCK_left-top + content_weight: + score: 0.25 + min_height_px: 200 + - position: left-bottom + template_id: MOCK_left-bottom + content_weight: + score: 0.25 + min_height_px: 200 + - position: right + template_id: MOCK_right + content_weight: + score: 0.5 + min_height_px: 200 + override_zone_geometries: + left-top: + x: 0 + y: 0 + w: 0.35 + h: 0.6 + left-bottom: + x: 0 + y: 0.6 + w: 0.35 + h: 0.4 + right: + x: 0.35 + y: 0 + w: 0.65 + h: 1.0 +expected_layout_css: + areas: '"left-top right" "left-bottom right"' + cols: 408px 758px + rows: 343px 228px + heights_px: + - 343 + - 228 + widths_px: + - 408 + - 758 + ratios: + - 0.6 + - 0.4 + width_ratios: + - 0.35 + - 0.65 + computation: user_override_geometry + dynamic_rows: true + dynamic_cols: true diff --git a/tests/phase_z2/fixtures/build_layout_css/top-1-bottom-2_default.yaml b/tests/phase_z2/fixtures/build_layout_css/top-1-bottom-2_default.yaml new file mode 100644 index 0000000..295b807 --- /dev/null +++ b/tests/phase_z2/fixtures/build_layout_css/top-1-bottom-2_default.yaml @@ -0,0 +1,38 @@ +input: + layout_preset: top-1-bottom-2 + zones_data: + - position: top + template_id: MOCK_top + content_weight: + score: 0.5 + min_height_px: 200 + - position: bottom-left + template_id: MOCK_bottom-left + content_weight: + score: 0.25 + min_height_px: 200 + - position: bottom-right + template_id: MOCK_bottom-right + content_weight: + score: 0.25 + min_height_px: 200 + override_zone_geometries: null +expected_layout_css: + areas: '"top top" "bottom-left bottom-right"' + cols: 583px 583px + rows: 314px 257px + heights_px: + - 314 + - 257 + widths_px: + - 583 + - 583 + ratios: + - 0.537 + - 0.439 + width_ratios: + - 0.494 + - 0.494 + computation: 2d_dynamic_aggregated + dynamic_rows: true + dynamic_cols: true diff --git a/tests/phase_z2/fixtures/build_layout_css/top-1-bottom-2_override.yaml b/tests/phase_z2/fixtures/build_layout_css/top-1-bottom-2_override.yaml new file mode 100644 index 0000000..7c0b272 --- /dev/null +++ b/tests/phase_z2/fixtures/build_layout_css/top-1-bottom-2_override.yaml @@ -0,0 +1,53 @@ +input: + layout_preset: top-1-bottom-2 + zones_data: + - position: top + template_id: MOCK_top + content_weight: + score: 0.5 + min_height_px: 200 + - position: bottom-left + template_id: MOCK_bottom-left + content_weight: + score: 0.25 + min_height_px: 200 + - position: bottom-right + template_id: MOCK_bottom-right + content_weight: + score: 0.25 + min_height_px: 200 + override_zone_geometries: + top: + x: 0 + y: 0 + w: 1.0 + h: 0.3 + bottom-left: + x: 0 + y: 0.3 + w: 0.5 + h: 0.7 + bottom-right: + x: 0.5 + y: 0.3 + w: 0.5 + h: 0.7 +expected_layout_css: + areas: '"top top" "bottom-left bottom-right"' + cols: 583px 583px + rows: 171px 400px + heights_px: + - 171 + - 400 + widths_px: + - 583 + - 583 + ratios: + - 0.3 + - 0.7 + width_ratios: + - 0.5 + - 0.5 + computation: user_override_geometry + dynamic_rows: true + dynamic_cols: true diff --git a/tests/phase_z2/fixtures/build_layout_css/top-2-bottom-1_default.yaml b/tests/phase_z2/fixtures/build_layout_css/top-2-bottom-1_default.yaml new file mode 100644 index 0000000..0752a54 --- /dev/null +++ b/tests/phase_z2/fixtures/build_layout_css/top-2-bottom-1_default.yaml @@ -0,0 +1,38 @@ +input: + layout_preset: top-2-bottom-1 + zones_data: + - position: top-left + template_id: MOCK_top-left + content_weight: + score: 0.25 + min_height_px: 200 + - position: top-right + template_id: MOCK_top-right + content_weight: + score: 0.25 + min_height_px: 200 + - position: bottom + template_id: MOCK_bottom + content_weight: + score: 0.5 + min_height_px: 200 + override_zone_geometries: null +expected_layout_css: + areas: '"top-left top-right" "bottom bottom"' + cols: 583px 583px + rows: 257px 314px + heights_px: + - 257 + - 314 + widths_px: + - 583 + - 583 + ratios: + - 0.439 + - 0.537 + width_ratios: + - 0.494 + - 0.494 + computation: 2d_dynamic_aggregated + dynamic_rows: true + dynamic_cols: true diff --git a/tests/phase_z2/fixtures/build_layout_css/top-2-bottom-1_override.yaml b/tests/phase_z2/fixtures/build_layout_css/top-2-bottom-1_override.yaml new file mode 100644 index 0000000..b44f36a --- /dev/null +++ b/tests/phase_z2/fixtures/build_layout_css/top-2-bottom-1_override.yaml @@ -0,0 +1,53 @@ +input: + layout_preset: top-2-bottom-1 + zones_data: + - position: top-left + template_id: MOCK_top-left + content_weight: + score: 0.25 + min_height_px: 200 + - position: top-right + template_id: MOCK_top-right + content_weight: + score: 0.25 + min_height_px: 200 + - position: bottom + template_id: MOCK_bottom + content_weight: + score: 0.5 + min_height_px: 200 + override_zone_geometries: + top-left: + x: 0 + y: 0 + w: 0.6 + h: 0.4 + top-right: + x: 0.6 + y: 0 + w: 0.4 + h: 0.4 + bottom: + x: 0 + y: 0.4 + w: 1.0 + h: 0.6 +expected_layout_css: + areas: '"top-left top-right" "bottom bottom"' + cols: 700px 466px + rows: 228px 343px + heights_px: + - 228 + - 343 + widths_px: + - 700 + - 466 + ratios: + - 0.4 + - 0.6 + width_ratios: + - 0.6 + - 0.4 + computation: user_override_geometry + dynamic_rows: true + dynamic_cols: true diff --git a/tests/phase_z2/fixtures/retry_gate/grid-2x2_dynamic_2d.yaml b/tests/phase_z2/fixtures/retry_gate/grid-2x2_dynamic_2d.yaml new file mode 100644 index 0000000..dced47d --- /dev/null +++ b/tests/phase_z2/fixtures/retry_gate/grid-2x2_dynamic_2d.yaml @@ -0,0 +1,24 @@ +case_id: grid-2x2_dynamic_2d +description: | + grid-2x2 (2x2 topology) is promoted to 2-D dynamic in IMP-09 PR 2. + Row-axis retry MUST be skipped by the gate with the + "dynamic_cols (2-D topology)" reason. +input_layout_css: + areas: '"top-left top-right" "bottom-left bottom-right"' + cols: 583px 583px + rows: 286px 285px + heights_px: [286, 285] + widths_px: [583, 583] + ratios: [0.489, 0.487] + width_ratios: [0.494, 0.494] + dynamic_rows: true + dynamic_cols: true +router_decision: + router_active: true + proposed_actions_summary: [zone_ratio_retry] +expected_gate: + retry_attempted: false + retry_skipped_reason_contains: + - "dynamic_cols" + - "2-D" + - "IMP-09" diff --git a/tests/phase_z2/fixtures/retry_gate/left-1-right-2_dynamic_2d.yaml b/tests/phase_z2/fixtures/retry_gate/left-1-right-2_dynamic_2d.yaml new file mode 100644 index 0000000..536ca80 --- /dev/null +++ b/tests/phase_z2/fixtures/retry_gate/left-1-right-2_dynamic_2d.yaml @@ -0,0 +1,24 @@ +case_id: left-1-right-2_dynamic_2d +description: | + left-1-right-2 (side-T-left topology) is promoted to 2-D dynamic in + IMP-09 PR 2. Row-axis retry MUST be skipped by the gate with the + "dynamic_cols (2-D topology)" reason. +input_layout_css: + areas: '"left right-top" "left right-bottom"' + cols: 777px 389px + rows: 286px 285px + heights_px: [286, 285] + widths_px: [777, 389] + ratios: [0.489, 0.487] + width_ratios: [0.658, 0.33] + dynamic_rows: true + dynamic_cols: true +router_decision: + router_active: true + proposed_actions_summary: [zone_ratio_retry] +expected_gate: + retry_attempted: false + retry_skipped_reason_contains: + - "dynamic_cols" + - "2-D" + - "IMP-09" diff --git a/tests/phase_z2/fixtures/retry_gate/left-2-right-1_dynamic_2d.yaml b/tests/phase_z2/fixtures/retry_gate/left-2-right-1_dynamic_2d.yaml new file mode 100644 index 0000000..6cc69bc --- /dev/null +++ b/tests/phase_z2/fixtures/retry_gate/left-2-right-1_dynamic_2d.yaml @@ -0,0 +1,24 @@ +case_id: left-2-right-1_dynamic_2d +description: | + left-2-right-1 (side-T-right topology) is promoted to 2-D dynamic in + IMP-09 PR 2. Row-axis retry MUST be skipped by the gate with the + "dynamic_cols (2-D topology)" reason. +input_layout_css: + areas: '"left-top right" "left-bottom right"' + cols: 389px 777px + rows: 286px 285px + heights_px: [286, 285] + widths_px: [389, 777] + ratios: [0.489, 0.487] + width_ratios: [0.33, 0.658] + dynamic_rows: true + dynamic_cols: true +router_decision: + router_active: true + proposed_actions_summary: [zone_ratio_retry] +expected_gate: + retry_attempted: false + retry_skipped_reason_contains: + - "dynamic_cols" + - "2-D" + - "IMP-09" diff --git a/tests/phase_z2/fixtures/retry_gate/top-1-bottom-2_dynamic_2d.yaml b/tests/phase_z2/fixtures/retry_gate/top-1-bottom-2_dynamic_2d.yaml new file mode 100644 index 0000000..0215ee9 --- /dev/null +++ b/tests/phase_z2/fixtures/retry_gate/top-1-bottom-2_dynamic_2d.yaml @@ -0,0 +1,26 @@ +case_id: top-1-bottom-2_dynamic_2d +description: | + top-1-bottom-2 (T topology) is promoted to 2-D dynamic in IMP-09 + PR 2 (dynamic_rows=True, dynamic_cols=True). Row-axis retry MUST be + skipped by the IMP-09 gate with the "dynamic_cols (2-D topology)" + skip reason, because row-only redistribution cannot reconcile both + axes simultaneously. +input_layout_css: + areas: '"top top" "bottom-left bottom-right"' + cols: 583px 583px + rows: 314px 257px + heights_px: [314, 257] + widths_px: [583, 583] + ratios: [0.537, 0.439] + width_ratios: [0.494, 0.494] + dynamic_rows: true + dynamic_cols: true +router_decision: + router_active: true + proposed_actions_summary: [zone_ratio_retry] +expected_gate: + retry_attempted: false + retry_skipped_reason_contains: + - "dynamic_cols" + - "2-D" + - "IMP-09" diff --git a/tests/phase_z2/fixtures/retry_gate/top-2-bottom-1_dynamic_2d.yaml b/tests/phase_z2/fixtures/retry_gate/top-2-bottom-1_dynamic_2d.yaml new file mode 100644 index 0000000..bb1e473 --- /dev/null +++ b/tests/phase_z2/fixtures/retry_gate/top-2-bottom-1_dynamic_2d.yaml @@ -0,0 +1,24 @@ +case_id: top-2-bottom-1_dynamic_2d +description: | + top-2-bottom-1 (inverted-T topology) is promoted to 2-D dynamic in + IMP-09 PR 2. Row-axis retry MUST be skipped by the gate with the + "dynamic_cols (2-D topology)" reason. +input_layout_css: + areas: '"top-left top-right" "bottom bottom"' + cols: 583px 583px + rows: 257px 314px + heights_px: [257, 314] + widths_px: [583, 583] + ratios: [0.439, 0.537] + width_ratios: [0.494, 0.494] + dynamic_rows: true + dynamic_cols: true +router_decision: + router_active: true + proposed_actions_summary: [zone_ratio_retry] +expected_gate: + retry_attempted: false + retry_skipped_reason_contains: + - "dynamic_cols" + - "2-D" + - "IMP-09" diff --git a/tests/phase_z2/test_build_layout_css_pr1.py b/tests/phase_z2/test_build_layout_css_pr1.py index 079d110..486865e 100644 --- a/tests/phase_z2/test_build_layout_css_pr1.py +++ b/tests/phase_z2/test_build_layout_css_pr1.py @@ -138,21 +138,21 @@ def test_vertical_2_override_keeps_fr_cols_legacy(): assert result["width_ratios"] == [0.4, 0.6] -# ────────────────────── fr_default sink (PR 1) ────────────────────── +# ────────────────────── 2-D dynamic dispatch (PR 2) ────────────────────── -def test_top_1_bottom_2_fr_default_populates_geometry(): - """T-shape (top-1-bottom-2) falls through to fr_default in PR 1 - but heights_px / widths_px must be populated (length-locked to - grid R=2, C=2).""" +def test_top_1_bottom_2_dynamic_2d_populates_geometry(): + """T-shape (top-1-bottom-2) is dispatched through the 2-D dynamic + builder in PR 2: heights_px / widths_px length-locked to grid + R=2, C=2 with both dynamic flags True.""" zones = [ _zone("top", 0.5), _zone("bottom-left", 0.25), _zone("bottom-right", 0.25), ] result = build_layout_css("top-1-bottom-2", zones) - assert result["computation"] == "fr_default_from_preset" - assert result["dynamic_rows"] is False - assert result["dynamic_cols"] is False + assert result["computation"] == "2d_dynamic_aggregated" + assert result["dynamic_rows"] is True + assert result["dynamic_cols"] is True assert len(result["heights_px"]) == 2 # R rows assert len(result["widths_px"]) == 2 # C cols