Add unit tests for geometry and fitness modules
26 tests for geometry (area, angles, aspect, boundary ids, centroid,
offset, etc.) and 35 tests for fitness (gaussian, config lookup,
quality terms, value rates, costs, stair helpers). Suite: 175 passed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 00:06:02 +01:00
|
|
|
"""Unit tests for geometry.py with known analytic values (oracle-free)."""
|
|
|
|
|
|
|
|
|
|
import math
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
2026-06-14 08:18:06 +01:00
|
|
|
from homemaker_layout import geometry
|
|
|
|
|
from homemaker_layout.dom import Node
|
Add unit tests for geometry and fitness modules
26 tests for geometry (area, angles, aspect, boundary ids, centroid,
offset, etc.) and 35 tests for fitness (gaussian, config lookup,
quality terms, value rates, costs, stair helpers). Suite: 175 passed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 00:06:02 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _square(size: float = 10.0) -> Node:
|
|
|
|
|
geometry.clear_cache()
|
|
|
|
|
return Node(node=[[0.0, 0.0], [size, 0.0], [size, size], [0.0, size]])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _rect(w: float, h: float) -> Node:
|
|
|
|
|
geometry.clear_cache()
|
|
|
|
|
return Node(node=[[0.0, 0.0], [w, 0.0], [w, h], [0.0, h]])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _divided(size: float = 10.0, split: float = 0.5):
|
|
|
|
|
"""Return (root, left, right) for a square split at `split`."""
|
|
|
|
|
geometry.clear_cache()
|
|
|
|
|
root = Node(
|
|
|
|
|
node=[[0.0, 0.0], [size, 0.0], [size, size], [0.0, size]],
|
|
|
|
|
division=[split, split],
|
|
|
|
|
)
|
|
|
|
|
left = Node(position="l")
|
|
|
|
|
right = Node(position="r")
|
|
|
|
|
root.left, root.right = left, right
|
|
|
|
|
left.parent = right.parent = root
|
|
|
|
|
return root, left, right
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_area_square():
|
|
|
|
|
assert geometry.area(_square(10.0)) == pytest.approx(100.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_area_rectangle():
|
|
|
|
|
assert geometry.area(_rect(6.0, 4.0)) == pytest.approx(24.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_area_divided_halves():
|
|
|
|
|
_, left, right = _divided(10.0, 0.5)
|
|
|
|
|
assert geometry.area(left) == pytest.approx(50.0)
|
|
|
|
|
assert geometry.area(right) == pytest.approx(50.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_area_divided_unequal():
|
|
|
|
|
_, left, right = _divided(10.0, 0.3)
|
|
|
|
|
assert geometry.area(left) == pytest.approx(30.0, rel=1e-9)
|
|
|
|
|
assert geometry.area(right) == pytest.approx(70.0, rel=1e-9)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_edge_length_square_all_equal():
|
|
|
|
|
r = _square(8.0)
|
|
|
|
|
for i in range(4):
|
|
|
|
|
assert geometry.edge_length(r, i) == pytest.approx(8.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_edge_length_rectangle():
|
|
|
|
|
r = _rect(6.0, 4.0)
|
|
|
|
|
assert geometry.edge_length(r, 0) == pytest.approx(6.0)
|
|
|
|
|
assert geometry.edge_length(r, 1) == pytest.approx(4.0)
|
|
|
|
|
assert geometry.edge_length(r, 2) == pytest.approx(6.0)
|
|
|
|
|
assert geometry.edge_length(r, 3) == pytest.approx(4.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_angle_square_corners_are_right_angles():
|
|
|
|
|
r = _square(10.0)
|
|
|
|
|
for i in range(4):
|
|
|
|
|
assert geometry.angle(r, i) == pytest.approx(math.pi / 2, abs=1e-9)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_angle_rectangle_corners_are_right_angles():
|
|
|
|
|
r = _rect(6.0, 4.0)
|
|
|
|
|
for i in range(4):
|
|
|
|
|
assert geometry.angle(r, i) == pytest.approx(math.pi / 2, abs=1e-9)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_aspect_square_is_one():
|
|
|
|
|
assert geometry.aspect(_square(10.0)) == pytest.approx(1.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_aspect_wide_rectangle():
|
|
|
|
|
assert geometry.aspect(_rect(2.0, 1.0)) == pytest.approx(2.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_aspect_tall_rectangle_is_same_as_wide():
|
|
|
|
|
assert geometry.aspect(_rect(1.0, 2.0)) == pytest.approx(2.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_length_narrowest_square():
|
|
|
|
|
assert geometry.length_narrowest(_square(5.0)) == pytest.approx(5.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_length_narrowest_rectangle():
|
|
|
|
|
assert geometry.length_narrowest(_rect(6.0, 3.0)) == pytest.approx(3.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_centroid_square():
|
|
|
|
|
cx, cy = geometry.centroid(_square(10.0))
|
|
|
|
|
assert cx == pytest.approx(5.0)
|
|
|
|
|
assert cy == pytest.approx(5.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_centroid_rectangle():
|
|
|
|
|
cx, cy = geometry.centroid(_rect(6.0, 4.0))
|
|
|
|
|
assert cx == pytest.approx(3.0)
|
|
|
|
|
assert cy == pytest.approx(2.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_boundary_id_root_returns_external_letters():
|
|
|
|
|
r = _square()
|
|
|
|
|
assert geometry.boundary_id(r, 0) == "a"
|
|
|
|
|
assert geometry.boundary_id(r, 1) == "b"
|
|
|
|
|
assert geometry.boundary_id(r, 2) == "c"
|
|
|
|
|
assert geometry.boundary_id(r, 3) == "d"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_boundary_id_left_child_internal_edge():
|
|
|
|
|
root, left, right = _divided()
|
|
|
|
|
# left child edge 1 (rid=1, position='l') → the division line → parent.id
|
|
|
|
|
assert geometry.boundary_id(left, 1) == root.id
|
|
|
|
|
assert geometry.boundary_id(left, 1) == "" # root id is '' (empty path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_boundary_id_right_child_internal_edge():
|
|
|
|
|
root, left, right = _divided()
|
|
|
|
|
# right child edge 3 → division line → parent.id
|
|
|
|
|
assert geometry.boundary_id(right, 3) == root.id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_boundary_id_children_inherit_external_edges():
|
|
|
|
|
_, left, right = _divided()
|
|
|
|
|
assert geometry.boundary_id(left, 0) == "a"
|
|
|
|
|
assert geometry.boundary_id(left, 3) == "d"
|
|
|
|
|
assert geometry.boundary_id(right, 0) == "a"
|
|
|
|
|
assert geometry.boundary_id(right, 1) == "b"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_is_between_2d_midpoint():
|
|
|
|
|
assert geometry.is_between_2d([5.0, 0.0], [0.0, 0.0], [10.0, 0.0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_is_between_2d_endpoint():
|
|
|
|
|
assert geometry.is_between_2d([0.0, 0.0], [0.0, 0.0], [10.0, 0.0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_is_between_2d_outside():
|
|
|
|
|
assert not geometry.is_between_2d([11.0, 0.0], [0.0, 0.0], [10.0, 0.0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_is_between_2d_none():
|
|
|
|
|
assert not geometry.is_between_2d(None, [0.0, 0.0], [10.0, 0.0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_offset_quad_inward_shrinks_area():
|
|
|
|
|
corners = [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]]
|
|
|
|
|
inset = geometry.offset_quad(corners, -1.0)
|
|
|
|
|
# inward offset of a square → smaller square
|
|
|
|
|
assert inset[0][0] > corners[0][0] # x moves right
|
|
|
|
|
assert inset[0][1] > corners[0][1] # y moves up
|
|
|
|
|
assert inset[2][0] < corners[2][0] # top-right x moves left
|
|
|
|
|
assert inset[2][1] < corners[2][1] # top-right y moves down
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_boundary_groups_single_leaf():
|
|
|
|
|
r = _square()
|
|
|
|
|
groups = geometry.boundary_groups(r)
|
|
|
|
|
# undivided root has no internal boundaries
|
|
|
|
|
assert len(groups) == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_boundary_groups_divided_root():
|
|
|
|
|
root, left, right = _divided()
|
|
|
|
|
groups = geometry.boundary_groups(root)
|
|
|
|
|
# one internal boundary between left and right
|
|
|
|
|
assert len(groups) == 1
|
|
|
|
|
contributors = list(groups.values())[0]
|
|
|
|
|
nodes = {leaf for leaf, _ in contributors}
|
|
|
|
|
assert left in nodes
|
|
|
|
|
assert right in nodes
|