"""Unit tests for src.placement.placement_engine.""" from __future__ import annotations import numpy as np from src.config.schema import PlacementConfig from src.placement.placement_engine import MIN_SIGNATURE_HEIGHT_PX, PlacementEngine from src.utils.random_utils import SeedManager def _synthetic_signature(w: int = 300, h: int = 150) -> np.ndarray: """A simple synthetic RGBA "signature": a diagonal opaque stroke on transparency.""" rgba = np.zeros((h, w, 4), dtype=np.uint8) for x in range(w): y = int((x / w) * (h - 1)) rgba[max(y - 3, 0):y + 3, x, :3] = (20, 20, 25) rgba[max(y - 3, 0):y + 3, x, 3] = 255 return rgba def test_placement_is_deterministic_for_same_seed(): cfg = PlacementConfig(rotation_deg={"min": -5, "max": 5}, scale_factor={"min": 0.7, "max": 1.0}) engine = PlacementEngine(cfg) sig = _synthetic_signature() r1 = engine.place(sig, target_box=(100, 100, 300, 200), canvas_size=(800, 600), seed_manager=SeedManager(1, 5)) r2 = engine.place(sig, target_box=(100, 100, 300, 200), canvas_size=(800, 600), seed_manager=SeedManager(1, 5)) assert r1.rotation_deg == r2.rotation_deg assert r1.scale_factor == r2.scale_factor assert r1.intended_bbox == r2.intended_bbox assert np.array_equal(r1.layer, r2.layer) def test_placement_differs_for_different_sample_index(): cfg = PlacementConfig(rotation_deg={"min": -5, "max": 5}, scale_factor={"min": 0.7, "max": 1.0}) engine = PlacementEngine(cfg) sig = _synthetic_signature() r1 = engine.place(sig, target_box=(100, 100, 300, 200), canvas_size=(800, 600), seed_manager=SeedManager(1, 1)) r2 = engine.place(sig, target_box=(100, 100, 300, 200), canvas_size=(800, 600), seed_manager=SeedManager(1, 2)) assert r1.rotation_deg != r2.rotation_deg or r1.scale_factor != r2.scale_factor def test_placement_respects_aspect_ratio_when_not_rotated(): cfg = PlacementConfig(rotation_deg=0.0, scale_factor=0.5, margin=0.0) engine = PlacementEngine(cfg) sig = _synthetic_signature(w=300, h=150) # aspect ratio 2:1 result = engine.place(sig, target_box=(0, 0, 400, 400), canvas_size=(800, 800), seed_manager=SeedManager(1, 1)) x1, y1, x2, y2 = result.intended_bbox placed_w, placed_h = x2 - x1, y2 - y1 assert abs((placed_w / placed_h) - 2.0) < 0.05 def test_placement_output_layer_matches_canvas_size(): cfg = PlacementConfig() engine = PlacementEngine(cfg) sig = _synthetic_signature() result = engine.place(sig, target_box=(10, 10, 200, 100), canvas_size=(640, 480), seed_manager=SeedManager(2, 0)) assert result.layer.shape == (480, 640, 4) def test_tiny_box_still_produces_a_legible_minimum_height_signature(): cfg = PlacementConfig(rotation_deg=0.0, scale_factor=1.0, margin=0.05) engine = PlacementEngine(cfg) sig = _synthetic_signature(w=300, h=150) # A box far shorter than the natural signature height. result = engine.place(sig, target_box=(0, 0, 300, 6), canvas_size=(800, 800), seed_manager=SeedManager(1, 1)) x1, y1, x2, y2 = result.intended_bbox assert (y2 - y1) >= MIN_SIGNATURE_HEIGHT_PX - 1 # allow rounding def test_misplacement_never_triggers_when_probability_zero(): cfg = PlacementConfig(misplacement_probability=0.0) engine = PlacementEngine(cfg) sig = _synthetic_signature() for i in range(20): result = engine.place(sig, target_box=(50, 50, 250, 150), canvas_size=(800, 600), seed_manager=SeedManager(1, i)) assert result.misplaced is False def test_misplacement_always_triggers_when_probability_one(): cfg = PlacementConfig(misplacement_probability=1.0, misplacement_strength=0.5) engine = PlacementEngine(cfg) sig = _synthetic_signature() for i in range(10): result = engine.place(sig, target_box=(50, 50, 250, 150), canvas_size=(800, 600), seed_manager=SeedManager(1, i)) assert result.misplaced is True