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(
|
def _compute_per_zone_geometry(
|
||||||
layout_css: dict,
|
layout_css: dict,
|
||||||
debug_zones: list[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],
|
def _build_cols_dynamic(preset: dict, zones_data: list[dict],
|
||||||
gap: int = GRID_GAP) -> dict:
|
gap: int = GRID_GAP) -> dict:
|
||||||
"""vertical-2 path — dynamic column widths, static fr row heights.
|
"""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"] 직접 사용.
|
# Layout preset → zone position 순서 = LAYOUT_PRESETS[preset]["positions"] 직접 사용.
|
||||||
# 이전 ZONE_POSITIONS_BY_PRESET (type-b 등 legacy 명) 는 dead code 로 제거 (2026-04-29).
|
# 이전 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,
|
"dynamic_cols": True,
|
||||||
"raw_zone_layout": {"override_applied": True, "source": override_zone_geometries},
|
"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:
|
else:
|
||||||
# PR 1 lock — warn-and-fallthrough preserved.
|
# warn-and-fallthrough preserved for remaining presets (single).
|
||||||
# PR 2 promotes this to strict ValueError via _override_to_grid_tracks.
|
# PR 3 territory.
|
||||||
print(
|
print(
|
||||||
f" [override-warning] zone-geometry override 는 layout '{layout_preset}' 미지원 "
|
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,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── Dynamic branch — topology dispatch (PR 1: rows / cols only) ──
|
# ── Dynamic branch — topology dispatch ──
|
||||||
if topology == "rows":
|
if topology == "rows":
|
||||||
return _build_rows_dynamic(preset, zones_data, gap)
|
return _build_rows_dynamic(preset, zones_data, gap)
|
||||||
if topology == "cols":
|
if topology == "cols":
|
||||||
return _build_cols_dynamic(preset, zones_data, gap)
|
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.
|
# PR 3 will dispatch single here.
|
||||||
return _build_fr_default(preset)
|
return _build_fr_default(preset)
|
||||||
|
|
||||||
@@ -3383,7 +3595,14 @@ def run_phase_z2_mvp1(
|
|||||||
dz["ratio"] = geo["zone_height_ratio"]
|
dz["ratio"] = geo["zone_height_ratio"]
|
||||||
dz["width_px"] = geo["zone_width_px"]
|
dz["width_px"] = geo["zone_width_px"]
|
||||||
dz["width_ratio"] = geo["zone_width_ratio"]
|
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']}")
|
print(f" zones : heights {layout_css['heights_px']} px, ratios {layout_css['ratios']}")
|
||||||
elif layout_css.get("dynamic_cols"):
|
elif layout_css.get("dynamic_cols"):
|
||||||
print(f" zones : widths {layout_css['widths_px']} px, width_ratios {layout_css['width_ratios']}")
|
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]
|
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():
|
def test_top_1_bottom_2_dynamic_2d_populates_geometry():
|
||||||
"""T-shape (top-1-bottom-2) falls through to fr_default in PR 1
|
"""T-shape (top-1-bottom-2) is dispatched through the 2-D dynamic
|
||||||
but heights_px / widths_px must be populated (length-locked to
|
builder in PR 2: heights_px / widths_px length-locked to grid
|
||||||
grid R=2, C=2)."""
|
R=2, C=2 with both dynamic flags True."""
|
||||||
zones = [
|
zones = [
|
||||||
_zone("top", 0.5),
|
_zone("top", 0.5),
|
||||||
_zone("bottom-left", 0.25),
|
_zone("bottom-left", 0.25),
|
||||||
_zone("bottom-right", 0.25),
|
_zone("bottom-right", 0.25),
|
||||||
]
|
]
|
||||||
result = build_layout_css("top-1-bottom-2", zones)
|
result = build_layout_css("top-1-bottom-2", zones)
|
||||||
assert result["computation"] == "fr_default_from_preset"
|
assert result["computation"] == "2d_dynamic_aggregated"
|
||||||
assert result["dynamic_rows"] is False
|
assert result["dynamic_rows"] is True
|
||||||
assert result["dynamic_cols"] is False
|
assert result["dynamic_cols"] is True
|
||||||
assert len(result["heights_px"]) == 2 # R rows
|
assert len(result["heights_px"]) == 2 # R rows
|
||||||
assert len(result["widths_px"]) == 2 # C cols
|
assert len(result["widths_px"]) == 2 # C cols
|
||||||
|
|||||||
Reference in New Issue
Block a user