"""geometry/transforms.py の仕様ベーステスト.
仕様出典:
- docs/04_SPEC/SPEC_01_アーキテクチャ設計.md(geometry 層の責務)
- docs/03_PLAN/PLAN_02_開発ステップ.md(Step 3 完了条件: 単位変換が可逆)
- docs/01_GUIDE/GUIDE_08_テスト方針.md(Mandatory: データ変換「変換が可逆であること」)
"""
import pathlib
import numpy
import pytest
from sm_tias_lightsim.geometry import LoadedMesh, mm_to_m, scale_vertices
# ---------------------------------------------------------------------------
# テストヘルパー
# ---------------------------------------------------------------------------
def _make_mesh(
vertices: numpy.ndarray,
unit: str = "mm",
) -> LoadedMesh:
"""テスト用 LoadedMesh を生成するヘルパー.faces はダミーの 1 面."""
faces = numpy.array([[0, 1, 2]], dtype=numpy.int64)
return LoadedMesh(
vertices=vertices,
faces=faces,
unit=unit, # type: ignore[arg-type]
source_path=pathlib.Path("/dummy/mesh.stl"),
)
# ---------------------------------------------------------------------------
# mm_to_m: スカラ
# ---------------------------------------------------------------------------
def test_mm_to_m_scalar_1000():
"""mm_to_m(1000.0) == 1.0 であること.
仕様: mm → m 変換(× 0.001)(transforms.py mm_to_m docstring)
"""
assert mm_to_m(1000.0) == pytest.approx(1.0)
def test_mm_to_m_scalar_2500_5():
"""mm_to_m(2500.5) == 2.5005 であること(小数点以下 4 桁)."""
assert mm_to_m(2500.5) == pytest.approx(2.5005)
def test_mm_to_m_zero():
"""mm_to_m(0) == 0 であること.
仕様: 0 ミリメートルは 0 メートル(恒等変換の特殊ケース)
"""
assert mm_to_m(0) == pytest.approx(0.0)
# ---------------------------------------------------------------------------
# mm_to_m: ndarray
# ---------------------------------------------------------------------------
def test_mm_to_m_ndarray():
"""mm_to_m(ndarray) が要素ごとに × 0.001 した配列を返すこと.
仕様: スカラ・ndarray 両対応 (transforms.py mm_to_m シグネチャ)
"""
input_arr = numpy.array([1000.0, 2000.0], dtype=numpy.float64)
result = mm_to_m(input_arr)
expected = numpy.array([1.0, 2.0], dtype=numpy.float64)
numpy.testing.assert_array_almost_equal(result, expected)
def test_mm_to_m_ndarray_shape_preserved():
"""mm_to_m が入力と同じ shape の配列を返すこと."""
arr = numpy.ones((3, 3), dtype=numpy.float64) * 500.0
result = mm_to_m(arr)
assert result.shape == (3, 3)
# ---------------------------------------------------------------------------
# scale_vertices: mm → m 変換
# ---------------------------------------------------------------------------
def test_scale_vertices_mm_to_m():
"""mm → m 変換で頂点値が 1/1000 になること.
仕様: mm → m 変換(× 0.001)(transforms.py scale_vertices)
"""
vertices_mm = numpy.array(
[[1000.0, 2000.0, 500.0], [0.0, 3000.0, 1500.0], [500.0, 500.0, 0.0]],
dtype=numpy.float64,
)
mesh = _make_mesh(vertices_mm, unit="mm")
scaled = scale_vertices(mesh, "m")
expected = vertices_mm * 0.001
numpy.testing.assert_array_almost_equal(scaled.vertices, expected)
# ---------------------------------------------------------------------------
# scale_vertices: m → mm 変換
# ---------------------------------------------------------------------------
def test_scale_vertices_m_to_mm():
"""m → mm 変換で頂点値が 1000 倍になること.
仕様: m → mm 変換(× 1000)(transforms.py scale_vertices)
"""
vertices_m = numpy.array(
[[1.0, 2.0, 0.5], [0.0, 3.0, 1.5], [0.5, 0.5, 0.0]],
dtype=numpy.float64,
)
mesh = _make_mesh(vertices_m, unit="m")
scaled = scale_vertices(mesh, "mm")
expected = vertices_m * 1000.0
numpy.testing.assert_array_almost_equal(scaled.vertices, expected)
# ---------------------------------------------------------------------------
# scale_vertices: 同一単位
# ---------------------------------------------------------------------------
def test_scale_vertices_same_unit_values_unchanged():
"""mm → mm では頂点値が変わらないこと."""
vertices = numpy.array([[100.0, 200.0, 300.0]], dtype=numpy.float64)
mesh = _make_mesh(vertices, unit="mm")
scaled = scale_vertices(mesh, "mm")
numpy.testing.assert_array_equal(scaled.vertices, vertices)
def test_scale_vertices_same_unit_returns_copy():
"""mm → mm では値は同じだが配列オブジェクトは別であること.
仕様: 元と同じ単位が指定された場合も頂点配列はコピーされる(関数の純粋性)
(transforms.py scale_vertices docstring)
"""
vertices = numpy.array([[100.0, 200.0, 300.0]], dtype=numpy.float64)
mesh = _make_mesh(vertices, unit="mm")
scaled = scale_vertices(mesh, "mm")
assert scaled.vertices is not mesh.vertices
# ---------------------------------------------------------------------------
# scale_vertices: unit フィールドの更新
# ---------------------------------------------------------------------------
def test_scale_vertices_unit_field_updated_mm_to_m():
"""mm → m 変換後の unit が "m" になること.
仕様: unit フィールドは target_unit に更新される (transforms.py scale_vertices docstring)
"""
vertices = numpy.array([[1000.0, 0.0, 0.0]], dtype=numpy.float64)
mesh = _make_mesh(vertices, unit="mm")
scaled = scale_vertices(mesh, "m")
assert scaled.unit == "m"
def test_scale_vertices_unit_field_updated_m_to_mm():
"""m → mm 変換後の unit が "mm" になること."""
vertices = numpy.array([[1.0, 0.0, 0.0]], dtype=numpy.float64)
mesh = _make_mesh(vertices, unit="m")
scaled = scale_vertices(mesh, "mm")
assert scaled.unit == "mm"
# ---------------------------------------------------------------------------
# scale_vertices: 可逆性(PLAN_02 完了条件「単位変換が可逆」)
# ---------------------------------------------------------------------------
def test_scale_vertices_roundtrip_is_identity():
"""mm → m → mm で元の頂点とほぼ一致すること(単位変換の可逆性).
仕様: 単位変換が可逆 (PLAN_02_開発ステップ.md Step 3 完了条件)
GUIDE_08 Mandatory: 「データ変換: 変換が可逆であること」
"""
vertices_mm = numpy.array(
[
[0.0, 0.0, 0.0],
[1000.0, 0.0, 0.0],
[500.0, 1000.0, 0.0],
[500.0, 500.0, 1000.0],
],
dtype=numpy.float64,
)
original = _make_mesh(vertices_mm, unit="mm")
converted = scale_vertices(original, "m")
round_tripped = scale_vertices(converted, "mm")
assert numpy.allclose(round_tripped.vertices, original.vertices)
# ---------------------------------------------------------------------------
# scale_vertices: インスタンスの独立性
# ---------------------------------------------------------------------------
def test_scale_vertices_returns_new_instance():
"""scale_vertices の戻り値が元と異なるインスタンスであること.
仕様: frozen dataclass の不変性を維持するため,常に新規インスタンスを返す
(transforms.py scale_vertices docstring)
"""
vertices = numpy.array([[1000.0, 2000.0, 0.0]], dtype=numpy.float64)
mesh = _make_mesh(vertices, unit="mm")
scaled = scale_vertices(mesh, "m")
assert scaled is not mesh
# ---------------------------------------------------------------------------
# scale_vertices: faces の不変性
# ---------------------------------------------------------------------------
def test_scale_vertices_faces_preserved():
"""単位変換で faces 配列の値が変わらないこと.
仕様: 頂点座標のみスケール変換する(面インデックスは変換対象外)
(SPEC_01 geometry 層の責務)
"""
faces = numpy.array([[0, 1, 2], [1, 2, 3]], dtype=numpy.int64)
vertices = numpy.array(
[
[1000.0, 0.0, 0.0],
[0.0, 1000.0, 0.0],
[0.0, 0.0, 1000.0],
[1000.0, 1000.0, 0.0],
],
dtype=numpy.float64,
)
mesh = LoadedMesh(
vertices=vertices,
faces=faces,
unit="mm",
source_path=pathlib.Path("/dummy/mesh.stl"),
)
scaled = scale_vertices(mesh, "m")
numpy.testing.assert_array_equal(scaled.faces, faces)
# ---------------------------------------------------------------------------
# scale_vertices: 元メッシュの faces 参照が同一かコピーかの確認
# ---------------------------------------------------------------------------
def test_scale_vertices_original_vertices_unchanged():
"""scale_vertices 後に元メッシュの頂点が変わっていないこと(純粋関数性).
仕様: 元の LoadedMesh は変更しない(frozen=True かつ純粋関数)
"""
vertices = numpy.array([[1000.0, 2000.0, 3000.0]], dtype=numpy.float64)
original_copy = vertices.copy()
mesh = _make_mesh(vertices, unit="mm")
scale_vertices(mesh, "m")
numpy.testing.assert_array_equal(mesh.vertices, original_copy)