feat(IMP-09): PR 2 — 2-D dynamic dispatch for 5 preset families
Stage 3 lock implementation: extend build_layout_css dispatch beyond
the horizontal-2 / vertical-2 1-D dynamic paths. T / inverted-T /
side-T-left / side-T-right / 2x2 now flow through a 2-D track solver
instead of the fr_default sink, with length-locked heights_px (R) +
widths_px (C) on every return path (default and override).
PR 2 scope (u1~u5):
- u1: _aggregate_zone_signals_per_track — per-row + per-col virtual
zones via max(weight) + max(min_height_px) of single-span zones,
falling back to all-span when a track has none.
- u2: _build_grid_dynamic_2d default builder — feeds virtual zones
into compute_zone_layout + compute_zone_layout_cols; emits
computation="2d_dynamic_aggregated", dynamic_rows=True,
dynamic_cols=True.
- u3: _override_to_grid_tracks override builder — single-span
aggregation (max h per row, max w per col), normalize, multiply
by avail_h/avail_w, last-element diff absorb; emits
computation="user_override_geometry"; falls back to u2 when
total_h or total_w == 0.
- u4: build_layout_css dispatcher wiring — topology in
{T, inverted-T, side-T-left, side-T-right, 2x2} routes to
_build_grid_dynamic_2d (default) or _override_to_grid_tracks
(override); legacy [override-warning] stderr removed for the
5 presets; step08 trace gains a 2-D-aware print line that fires
before the dynamic_rows / dynamic_cols branches.
- u5: PR 1 lock test test_top_1_bottom_2_fr_default_populates_geometry
renamed to test_top_1_bottom_2_dynamic_2d_populates_geometry and
flipped to PR 2 reality (computation="2d_dynamic_aggregated",
dynamic_rows=True, dynamic_cols=True).
Fixtures: 10 build_layout_css (5 presets × {default, override}) +
5 retry_gate *_dynamic_2d.yaml locking the retry gate skip reason
"dynamic_cols (2-D topology) ... IMP-09 lock" for the 5 presets.
Tests: python -m pytest -q tests = 104 passed (Stage 2 baseline
10 RED → GREEN, 0 regressions). Kei archive
(build_containers_type_b / page_structure) untouched —
rg "build_containers_type_b|page_structure" src/phase_z2_pipeline.py
returns 0 hits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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']}")
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
24
tests/phase_z2/fixtures/retry_gate/grid-2x2_dynamic_2d.yaml
Normal file
24
tests/phase_z2/fixtures/retry_gate/grid-2x2_dynamic_2d.yaml
Normal file
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user