Newer
Older
SmTIAS-LightSim / tests / scene / test_sensors.py
"""scene/sensors.py の仕様ベーステスト.

仕様出典:
- docs/04_SPEC/SPEC_02_シーン定義仕様.md(sensor セクション)
- docs/03_PLAN/PLAN_02_開発ステップ.md(Step 4: scene/sensors.py の役割)

build_sensor_dict の戻り値 dict 構造のみを検証する.Mitsuba の load_dict は呼ばない.
ただし mi.ScalarTransform4f を内部で使うため mitsuba のインポートは発生する.
"""

from __future__ import annotations

import pytest

from sm_tias_lightsim.scene.schema import FilmConfig, SensorConfig
from sm_tias_lightsim.scene.sensors import build_sensor_dict


def _make_sensor(
    *,
    position=(0.0, 0.0, 100.0),
    target=(0.0, 0.0, 0.0),
    up=(0.0, 1.0, 0.0),
    fov_deg=60.0,
    width=640,
    height=480,
    rfilter="gaussian",
) -> SensorConfig:
    """テスト用 SensorConfig を組み立てるヘルパ."""
    return SensorConfig(
        type="perspective",
        position=position,
        target=target,
        up=up,
        fov_deg=fov_deg,
        film=FilmConfig(width=width, height=height, rfilter=rfilter),
    )


# ---------------------------------------------------------------------------
# type
# ---------------------------------------------------------------------------


def test_sensor_type_is_perspective():
    """build_sensor_dict の戻り値 type が "perspective" であること.

    仕様: sensor.type は perspective のみ対応 (SPEC_02 sensor セクション)
    """
    sensor = _make_sensor()
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert result["type"] == "perspective"


# ---------------------------------------------------------------------------
# fov
# ---------------------------------------------------------------------------


def test_sensor_fov_matches_fov_deg():
    """fov_deg がそのまま fov キーに格納されること.

    仕様: fov_deg → fov (SPEC_02 sensor セクション)
    """
    sensor = _make_sensor(fov_deg=45.0)
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert result["fov"] == pytest.approx(45.0)


# ---------------------------------------------------------------------------
# film
# ---------------------------------------------------------------------------


def test_sensor_film_width_and_height():
    """film の width / height が正しく dict に格納されること.

    仕様: film.width, film.height (SPEC_02 sensor セクション)
    """
    sensor = _make_sensor(width=1920, height=1080)
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert result["film"]["width"] == 1920
    assert result["film"]["height"] == 1080


def test_sensor_film_type_is_hdrfilm():
    """film の type が "hdrfilm" であること.

    仕様: Mitsuba の HDR フィルム型 (SPEC_01 render/output 責務)
    """
    sensor = _make_sensor()
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert result["film"]["type"] == "hdrfilm"


def test_sensor_film_rfilter_gaussian():
    """rfilter が "gaussian" のとき rfilter dict の type が "gaussian" になること.

    仕様: film.rfilter (SPEC_02 sensor セクション)
    """
    sensor = _make_sensor(rfilter="gaussian")
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert result["film"]["rfilter"]["type"] == "gaussian"


def test_sensor_film_rfilter_box():
    """rfilter が "box" のとき rfilter dict の type が "box" になること.

    仕様: film.rfilter: gaussian | box | tent (SPEC_02 sensor セクション FilmConfig)
    """
    sensor = _make_sensor(rfilter="box")
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert result["film"]["rfilter"]["type"] == "box"


# ---------------------------------------------------------------------------
# to_world
# ---------------------------------------------------------------------------


def test_sensor_to_world_key_exists():
    """build_sensor_dict の戻り値に to_world キーが存在すること.

    仕様: perspective sensor は to_world で視点変換を持つ (SPEC_01 scene/sensors 責務)
    """
    sensor = _make_sensor()
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert "to_world" in result


def test_sensor_position_scale_affects_to_world():
    """position_scale が異なると to_world が変わること(文字列化で差分確認).

    仕様: position_scale で position/target が変換される (SPEC_01 scene/sensors 責務)
    """
    sensor = _make_sensor(position=(0.0, 0.0, 100.0), target=(0.0, 0.0, 0.0))
    result_mm = build_sensor_dict(sensor, position_scale=0.001)
    result_m = build_sensor_dict(sensor, position_scale=1.0)
    assert str(result_mm["to_world"]) != str(result_m["to_world"])


# ---------------------------------------------------------------------------
# sampler
# ---------------------------------------------------------------------------


def test_sensor_sampler_type_is_independent():
    """sampler の type が "independent" であること.

    仕様: spp は render 時に渡す (runner.py 設計)
    """
    sensor = _make_sensor()
    result = build_sensor_dict(sensor, position_scale=0.001)
    assert result["sampler"]["type"] == "independent"


# ---------------------------------------------------------------------------
# 境界値: 様々な position_scale
# ---------------------------------------------------------------------------


def test_sensor_position_scale_1_leaves_position_in_to_world():
    """position_scale=1.0 のとき position_scale=0.001 と to_world が異なること.

    座標が 1000 倍差があれば to_world 行列も異なるはず.
    """
    sensor_mm = _make_sensor(position=(0.0, 0.0, 500.0), target=(0.0, 0.0, 0.0))
    result_mm = build_sensor_dict(sensor_mm, position_scale=0.001)
    result_m = build_sensor_dict(sensor_mm, position_scale=1.0)
    assert str(result_mm["to_world"]) != str(result_m["to_world"])