File size: 6,177 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
import json

import pytest
import torch

from fastfill.v2.io import ensure_disjoint, read_samples, safe_output
from fastfill.v2.losses import GeometryCriterion, LossConfig
from fastfill.v2.tests.test_supervision import batch, prediction


def sample():
    return {"schema_version": "fastfill.v2", "condition": {"schema_version": "fastfill.v2", "room": {
        "frame": "right_handed_z_up", "floor_polygon_xy_m": [[0, 0], [5, 0], [5, 4], [0, 4]],
        "floor_z_m": 0., "floor_known": True, "height_m": 2.8},
        "objects": [{"id": "a", "category": "chair", "description": "a chair", "support_parent": "floor"}], "constraints": []},
        "target": {"schema_version": "fastfill.v2", "objects": [{"id": "a", "target_size_local_m": [1., 1., 1.],
          "bottom_center_m": [2., 1., 0.], "yaw_rad": 0.}]},
        "validity": {"position": [[True] * 3], "size": [[True] * 3], "yaw": [True], "yaw_symmetry_order": [1]},
        "provenance": {"source": "fixture", "house_id": "h1", "scene_id": "s1", "split": "train"}}


def test_io_source_protection_split_guard(tmp_path):
    with pytest.raises(ValueError):
        safe_output("/Volumes/harddisk/3D_Room_Collections/test-output")
    with pytest.raises(FileExistsError):
        safe_output(tmp_path)
    row = sample()
    with pytest.raises(ValueError):
        ensure_disjoint([row], [row])
    path = tmp_path / "data.jsonl"
    path.write_text(json.dumps({**row, "provenance": {**row["provenance"], "split": "test"}}) + "\n")
    with pytest.raises(ValueError):
        read_samples(path, training=True)


def test_optional_regularizers_are_separate_and_differentiable():
    b = batch()
    b["conditions"] = [{"objects": b["objects"][0], "constraints": [], "room": {
        "floor_polygon_xy_m": [[0, 0], [1, 0], [1, 1], [0, 1]], "boundary_known": True}}]
    p = prediction(b)
    result = GeometryCriterion(LossConfig(collision=1., boundary=1., box=1.))(p, b)
    assert result["collision"] > 0
    assert result["boundary"] > 0
    assert result["box"] > 0
    result["loss"].backward()
    assert torch.isfinite(p["position_normalized"].grad).all()
    b["conditions"][0]["room"]["boundary_known"] = False
    with pytest.raises(ValueError, match="unknown"):
        GeometryCriterion(LossConfig(boundary=1.))(p, b)


def test_train_checkpoint_inference_evaluation_end_to_end(tmp_path):
    from fastfill.v2.train import run_training
    from fastfill.v2.evaluate import run_evaluation
    row = sample()
    path = tmp_path / "train.jsonl"
    path.write_text(json.dumps(row) + "\n")
    config = {"model": {"backbone": "tiny", "decoder_dim": 16, "decoder_heads": 2,
               "decoder_layers": 1, "tiny_hidden_size": 16, "lora_rank": 0},
              "training": {"cpu": True, "steps": 20, "batch_size": 1, "learning_rate": .01,
               "max_length": 4096, "checkpoint_every": 0, "validate_every": 0, "cpu_threads": 1}}
    run = tmp_path / "trained"
    logs = run_training(config, path, run)
    assert logs[-1]["loss"] < logs[0]["loss"]
    assert all(logs[0]["gradient_norms"][name] > 0 for name in ("decoder", "backbone", "size_head", "position_head", "yaw_logits_head", "yaw_residual_head"))
    test_path = tmp_path / "test.jsonl"
    test_path.write_text(json.dumps({**row, "provenance": {**row["provenance"], "split": "test", "house_id": "h2"}}) + "\n")
    report = run_evaluation(test_path, tmp_path / "evaluation", checkpoint=run / "model")
    assert report["requests"] == 1
    assert report["model"]["schema_success"] == 1
    assert report["model"]["reference"]["log_size_error"]["mean"] is not None
    assert report["model"]["reference"]["bev_iou"]["valid_objects"] == 1
    with pytest.raises(FileExistsError):
        run_training(config, path, run, dry_run=True)


def test_evaluation_keeps_failed_requests_and_atomic_asset_stages(tmp_path):
    from fastfill.v2.evaluate import run_evaluation
    row = sample()
    data = tmp_path / "evaluation.jsonl"
    data.write_text(json.dumps(row) + "\n" + json.dumps(row) + "\n")
    layouts = tmp_path / "predictions.jsonl"
    layouts.write_text(json.dumps(row["target"]) + "\n" + json.dumps({"schema_version": "fastfill.v2", "objects": []}) + "\n")
    catalog = tmp_path / "catalog.json"
    catalog.write_text(json.dumps([{"ref": "chair-asset", "category": "chair", "actual_size_local_m": [1, 1, 1],
                                    "semantic_front_local": [1, 0, 0], "capabilities": []}]))
    report = run_evaluation(data, tmp_path / "results", predictions=layouts, catalog=catalog, commit_in_memory=True)
    assert report["requests"] == 2
    assert report["model"]["schema_success"] == .5
    assert report["system"]["final_commit"] == .5
    assert report["failed_requests"] == 1
    assert report["model"]["reference"]["bottom_center_error_m"]["mean"] == 0
    records = [json.loads(line) for line in (tmp_path / "results/outcomes.jsonl").read_text().splitlines()]
    assert records[0]["runtime"]["raw_model_output"] == row["target"]
    assert records[0]["runtime"]["metrics"]["asset"]["first_pass_actual_geometry_validation"]


def test_global_count_compensates_ddp_gradient_average(monkeypatch):
    from fastfill.v2.losses import _mean
    monkeypatch.setattr(torch.distributed, "is_initialized", lambda: True)
    monkeypatch.setattr(torch.distributed, "get_world_size", lambda: 2)
    # Local rank has 1 label; peer has 3: global denominator is 4.
    monkeypatch.setattr(torch.distributed, "all_reduce", lambda t: t.add_(3))
    local_sum = torch.tensor(6., requires_grad=True)
    value, count = _mean(local_sum, 1)
    assert count == 4
    value.backward()
    assert local_sum.grad.item() == .5  # DDP later averages by 2 -> 1/4 per-label gradient.


def test_invalid_target_geometry_is_failure_with_schema_success():
    from fastfill.v2.evaluate import summarize
    report = summarize([{"model": {"schema_success": True, "requested_ids_exactly_once": True,
                         "positive_valid_size": True, "target_geometry_valid": False},
                         "evaluation_wall_time_ms": 1.}])
    assert report["failed_requests"] == 1
    assert report["inference_failed_requests"] == 0