File size: 8,515 Bytes
e6f80f3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""Reference room extents retain coordinates without claiming a measured room."""
from copy import deepcopy
import json
from pathlib import Path
import subprocess
import sys

import pytest
import torch

from fastfill.v2.direct_layout import bbox_diagnostics, layout_to_roomgenbench, layout_to_scene, request_to_condition
from fastfill.v2.schema import validate_condition, validate_layout


def request(dimensions=(5., 4.)):
    return {"room_type": "unknown", "room_size_m": list(dimensions), "furniture_list": ["chair"]}


def prediction(condition, position=(-1., 2., -.5)):
    return {"schema_version": "fastfill.v2", "objects": [
        {"id": condition["objects"][0]["id"], "target_size_local_m": [1., 1., 1.],
         "bottom_center_m": list(position), "yaw_rad": 0.}]}


@pytest.mark.parametrize("dimensions", [(5., 4.), (5., 4., 3.)])
def test_reference_extent_preserves_request_without_fabricating_boundary_or_floor(dimensions):
    source = request(dimensions)
    saved = deepcopy(source)
    condition = request_to_condition(source, room_size_semantics="reference_extent")
    validate_condition(condition)
    assert source == saved
    assert set(source) == {"room_type", "room_size_m", "furniture_list"}
    assert condition["room"] == {
        "frame": "right_handed_z_up", "room_type": "unknown",
        "floor_polygon_xy_m": [[0., 0.], [5., 0.], [5., 4.], [0., 4.]],
        "floor_z_m": 0., "floor_known": False, "boundary_known": False,
        "boundary_quality": "source_reference_extent",
        "height_m": dimensions[2] if len(dimensions) == 3 else None}
    assert condition["constraints"] == []
    assert condition["objects"] == [{"id": "obj_0000", "category": "chair", "description": "chair"}]
    scene = layout_to_scene(condition, prediction(condition))
    assert scene["room"] == condition["room"]
    scene["room"]["floor_polygon_xy_m"][0][0] = 100.
    assert condition["room"]["floor_polygon_xy_m"][0][0] == 0.


def test_default_and_explicit_rectangular_profiles_remain_exact_and_independent():
    source = request((5., 4., 3.))
    saved = deepcopy(source)
    default = request_to_condition(source)
    explicit = request_to_condition(source, room_size_semantics="rectangular")
    reference = request_to_condition(source, room_size_semantics="reference_extent")
    assert source == saved and default == explicit
    assert default["room"]["floor_known"] is True
    assert default["room"]["boundary_known"] is True
    assert default["room"]["boundary_quality"] == "explicit_rectangular_request"
    assert {key: value for key, value in reference["room"].items()
            if key not in {"floor_known", "boundary_known", "boundary_quality"}} == {
                key: value for key, value in default["room"].items()
                if key not in {"floor_known", "boundary_known", "boundary_quality"}}


@pytest.mark.parametrize("semantics", [None, True, "extent", "", "REFERENCE_EXTENT"])
def test_unknown_room_size_semantics_is_rejected(semantics):
    with pytest.raises(ValueError, match="room_size_semantics"):
        request_to_condition(request(), room_size_semantics=semantics)


@pytest.mark.parametrize("semantics,expected", [("rectangular", "fail"), ("reference_extent", "unknown")])
def test_boundary_floor_and_ceiling_diagnostics_obey_profile(semantics, expected):
    condition = request_to_condition(request((5., 4., 3.)), room_size_semantics=semantics)
    # One outside object below nominal z=0; a second case above nominal ceiling.
    for position in [(-1., 2., -.5), (-1., 2., 4.)]:
        scene = layout_to_scene(condition, prediction(condition, position))
        report = bbox_diagnostics(scene)
        checks = {check["code"]: check["status"] for check in report["checks"]}
        assert checks["boundary"] == expected
        assert checks["floor_lower_bound"] == (expected if position[2] < 0 else (
            "pass" if semantics == "rectangular" else "unknown"))
        assert checks["ceiling"] == (expected if position[2] > 3 else (
            "pass" if semantics == "rectangular" else "unknown"))
        assert report["asset_retrieval"] == "not_attempted"
        assert report["support"] == "unknown" and report["commit"] == "not_attempted"
        if semantics == "reference_extent":
            assert report["room_size_semantics"] == "reference_extent"


def test_reference_extent_prediction_cli_exports_raw_geometry_with_unknown_physical_checks(tmp_path):
    from fastfill.v2.batch import TinyTokenizer
    from fastfill.v2.model import ModelConfig, build_model

    checkpoint = tmp_path / "model"
    model = build_model(ModelConfig(backbone="tiny", lora_rank=0, decoder_dim=16,
                        decoder_heads=2, decoder_layers=1, tiny_hidden_size=16))
    with torch.no_grad():
        for head in (model.position_head, model.size_head, model.yaw_logits_head, model.yaw_residual_head):
            head.weight.zero_()
            head.bias.zero_()
        model.position_head.bias.copy_(torch.tensor([-.2, .5, -.5]))
    model.save_pretrained(checkpoint)
    TinyTokenizer().save_pretrained(tmp_path / "tokenizer")
    source = request()
    request_path = tmp_path / "request.json"
    request_path.write_text(json.dumps(source))
    output, handoff = tmp_path / "prediction.json", tmp_path / "handoff"
    command = [sys.executable, "-m", "fastfill.v2.predict", "--checkpoint", str(checkpoint),
               "--request", str(request_path), "--room-size-semantics", "reference_extent",
               "--output", str(output), "--export-dir", str(handoff), "--device", "cpu"]
    proc = subprocess.run(command, cwd=Path(__file__).resolve().parents[3],
                          capture_output=True, text=True, timeout=30)
    assert proc.returncode == 0, proc.stderr
    condition = request_to_condition(source, room_size_semantics="reference_extent")
    actual = json.loads(output.read_text())
    validate_layout(actual, condition)
    assert actual["objects"][0]["bottom_center_m"][0] < 0.
    assert actual["objects"][0]["bottom_center_m"][2] < 0.
    scene = json.loads((handoff / "scene.json").read_text())
    assert scene == layout_to_scene(condition, actual)
    assert scene["room"]["height_m"] is None
    assert not scene["room"]["boundary_known"] and not scene["room"]["floor_known"]
    diagnostics = json.loads((handoff / "diagnostics.json").read_text())
    assert {check["status"] for check in diagnostics["checks"]} == {"unknown"}
    assert diagnostics["room_size_semantics"] == "reference_extent"
    assert diagnostics["asset_retrieval"] == "not_attempted"
    assert diagnostics["commit"] == "not_attempted"
    assert json.loads(request_path.read_text()) == source
    assert (handoff / "layout.glb").exists() and (handoff / "preview.svg").exists()


def test_roomgenbench_handoff_keeps_reference_range_distinct_from_physical_room():
    condition = request_to_condition(request((5., 4., 3.)), room_size_semantics="reference_extent")
    actual = prediction(condition)
    saved_condition, saved_prediction = deepcopy(condition), deepcopy(actual)
    downstream = layout_to_roomgenbench(condition, actual)
    assert downstream["room_size_semantics"] == "reference_extent"
    assert downstream["room"]["boundary_known"] is False
    assert downstream["room"]["floor_known"] is False
    assert downstream["room"]["boundary_quality"] == "source_reference_extent"
    assert "reference" in downstream["room_interpretation"]
    assert downstream["room"]["dimensions"] == {"width": 5., "length": 4., "height": 3.}
    assert downstream["objects"][0]["position"] == {"x": -1., "y": 2., "z": -.5}
    assert condition == saved_condition and actual == saved_prediction
    strict = request_to_condition(request((5., 4., 3.)))
    assert "room_size_semantics" not in layout_to_roomgenbench(strict, prediction(strict))


def test_prediction_rejects_reference_flag_with_full_condition_before_model_loading(tmp_path, monkeypatch):
    from fastfill.v2 import predict

    def forbidden_load(*args, **kwargs):
        raise AssertionError("inapplicable semantics flags must be rejected before model load")

    monkeypatch.setattr(predict, "load_model", forbidden_load)
    with pytest.raises(ValueError, match="only to --request"):
        predict.main(["--checkpoint", str(tmp_path / "missing-model"),
                      "--condition", str(tmp_path / "missing-condition.json"),
                      "--room-size-semantics", "reference_extent", "--output", str(tmp_path / "prediction.json")])