216 lines
7.8 KiB
Python
216 lines
7.8 KiB
Python
import pytest
|
|
from shapely.affinity import rotate as shapely_rotate
|
|
from shapely.affinity import scale as shapely_scale
|
|
from shapely.affinity import translate as shapely_translate
|
|
from shapely.geometry import Polygon
|
|
|
|
from inire.geometry.components import Bend90, SBend, Straight
|
|
from inire.geometry.primitives import Port, rotate_port, translate_port
|
|
|
|
|
|
def test_straight_generation() -> None:
|
|
start = Port(0, 0, 0)
|
|
length = 10.0
|
|
width = 2.0
|
|
result = Straight.generate(start, length, width)
|
|
|
|
assert result.end_port.x == 10.0
|
|
assert result.end_port.y == 0.0
|
|
assert result.end_port.orientation == 0.0
|
|
assert len(result.geometry) == 1
|
|
|
|
# Bounds of the polygon
|
|
minx, miny, maxx, maxy = result.geometry[0].bounds
|
|
assert minx == 0.0
|
|
assert maxx == 10.0
|
|
assert miny == -1.0
|
|
assert maxy == 1.0
|
|
|
|
|
|
def test_bend90_generation() -> None:
|
|
start = Port(0, 0, 0)
|
|
radius = 10.0
|
|
width = 2.0
|
|
|
|
# CW bend
|
|
result_cw = Bend90.generate(start, radius, width, direction="CW")
|
|
assert result_cw.end_port.x == 10.0
|
|
assert result_cw.end_port.y == -10.0
|
|
assert result_cw.end_port.orientation == 270.0
|
|
|
|
# CCW bend
|
|
result_ccw = Bend90.generate(start, radius, width, direction="CCW")
|
|
assert result_ccw.end_port.x == 10.0
|
|
assert result_ccw.end_port.y == 10.0
|
|
assert result_ccw.end_port.orientation == 90.0
|
|
|
|
|
|
def test_sbend_generation() -> None:
|
|
start = Port(0, 0, 0)
|
|
offset = 5.0
|
|
radius = 10.0
|
|
width = 2.0
|
|
|
|
result = SBend.generate(start, offset, radius, width)
|
|
assert result.end_port.y == 5.0
|
|
assert result.end_port.orientation == 0.0
|
|
assert len(result.geometry) == 2 # Optimization: returns individual arcs
|
|
|
|
# Verify failure for large offset
|
|
with pytest.raises(ValueError, match=r"SBend offset .* must be less than 2\*radius"):
|
|
SBend.generate(start, 25.0, 10.0, 2.0)
|
|
|
|
|
|
def test_sbend_generation_negative_offset_keeps_second_arc_below_centerline() -> None:
|
|
start = Port(0, 0, 0)
|
|
offset = -5.0
|
|
radius = 10.0
|
|
width = 2.0
|
|
|
|
result = SBend.generate(start, offset, radius, width)
|
|
|
|
assert result.end_port.y == -5.0
|
|
second_arc_minx, second_arc_miny, second_arc_maxx, second_arc_maxy = result.geometry[1].bounds
|
|
assert second_arc_maxy <= width / 2.0 + 1e-6
|
|
assert second_arc_miny < -width / 2.0
|
|
|
|
|
|
def test_bend_collision_models() -> None:
|
|
start = Port(0, 0, 0)
|
|
radius = 10.0
|
|
width = 2.0
|
|
|
|
# 1. BBox model
|
|
res_bbox = Bend90.generate(start, radius, width, direction="CCW", collision_type="bbox")
|
|
# Arc CCW R=10 from (0,0,0) ends at (10,10,90).
|
|
# Waveguide width is 2.0, so bbox will be slightly larger than (0,0,10,10)
|
|
minx, miny, maxx, maxy = res_bbox.geometry[0].bounds
|
|
assert minx <= 0.0 + 1e-6
|
|
assert maxx >= 10.0 - 1e-6
|
|
assert miny <= 0.0 + 1e-6
|
|
assert maxy >= 10.0 - 1e-6
|
|
|
|
# 2. Clipped BBox model
|
|
res_clipped = Bend90.generate(start, radius, width, direction="CCW", collision_type="clipped_bbox", clip_margin=1.0)
|
|
# Conservative 8-point approximation should still be tighter than the full bbox.
|
|
assert len(res_clipped.geometry[0].exterior.coords) - 1 == 8
|
|
assert res_clipped.geometry[0].area < res_bbox.geometry[0].area
|
|
|
|
# It should also conservatively contain the true arc.
|
|
res_arc = Bend90.generate(start, radius, width, direction="CCW", collision_type="arc")
|
|
assert res_clipped.geometry[0].covers(res_arc.geometry[0])
|
|
|
|
|
|
def test_custom_bend_collision_polygon_uses_local_transform() -> None:
|
|
custom_poly = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)])
|
|
|
|
cases = [
|
|
(Port(0, 0, 0), "CCW", (0.0, 10.0), 0.0, False),
|
|
(Port(0, 0, 0), "CW", (0.0, -10.0), 0.0, True),
|
|
(Port(0, 0, 90), "CCW", (-10.0, 0.0), 90.0, False),
|
|
]
|
|
|
|
for start, direction, center_xy, rotation_deg, mirror_y in cases:
|
|
result = Bend90.generate(start, 10.0, 2.0, direction=direction, collision_type=custom_poly)
|
|
expected = custom_poly
|
|
if mirror_y:
|
|
expected = shapely_scale(expected, xfact=1.0, yfact=-1.0, origin=(0.0, 0.0))
|
|
if rotation_deg:
|
|
expected = shapely_rotate(expected, rotation_deg, origin=(0.0, 0.0), use_radians=False)
|
|
expected = shapely_translate(expected, center_xy[0], center_xy[1])
|
|
|
|
assert result.geometry[0].symmetric_difference(expected).area < 1e-6
|
|
assert result.actual_geometry is not None
|
|
assert result.actual_geometry[0].symmetric_difference(expected).area < 1e-6
|
|
|
|
|
|
def test_custom_bend_collision_polygon_becomes_actual_geometry() -> None:
|
|
custom_poly = Polygon([(0, -11), (11, -11), (11, 0), (9, 0), (9, -9), (0, -9)])
|
|
result = Bend90.generate(Port(0, 0, 0), 10.0, 2.0, direction="CCW", collision_type=custom_poly, dilation=1.0)
|
|
|
|
assert result.actual_geometry is not None
|
|
assert result.dilated_actual_geometry is not None
|
|
assert result.geometry[0].symmetric_difference(result.actual_geometry[0]).area < 1e-6
|
|
assert result.dilated_geometry is not None
|
|
assert result.dilated_geometry[0].symmetric_difference(result.dilated_actual_geometry[0]).area < 1e-6
|
|
|
|
|
|
def test_sbend_collision_models() -> None:
|
|
start = Port(0, 0, 0)
|
|
offset = 5.0
|
|
radius = 10.0
|
|
width = 2.0
|
|
|
|
res_bbox = SBend.generate(start, offset, radius, width, collision_type="bbox")
|
|
# Geometry should be a list of individual bbox polygons for each arc
|
|
assert len(res_bbox.geometry) == 2
|
|
|
|
res_arc = SBend.generate(start, offset, radius, width, collision_type="arc")
|
|
area_bbox = sum(p.area for p in res_bbox.geometry)
|
|
area_arc = sum(p.area for p in res_arc.geometry)
|
|
assert area_bbox > area_arc
|
|
|
|
|
|
def test_sbend_continuity() -> None:
|
|
# Verify SBend endpoints and continuity math
|
|
start = Port(10, 20, 90) # Starting facing up
|
|
offset = 4.0
|
|
radius = 20.0
|
|
width = 1.0
|
|
|
|
res = SBend.generate(start, offset, radius, width)
|
|
|
|
# Target orientation should be same as start
|
|
assert abs(res.end_port.orientation - 90.0) < 1e-6
|
|
|
|
# For a port at 90 deg, +offset is a shift in -x direction
|
|
assert abs(res.end_port.x - (10.0 - offset)) < 1e-6
|
|
|
|
# Geometry should be a list of valid polygons
|
|
assert len(res.geometry) == 2
|
|
for p in res.geometry:
|
|
assert p.is_valid
|
|
|
|
|
|
def test_arc_sagitta_precision() -> None:
|
|
# Verify that requested sagitta actually controls segment count
|
|
start = Port(0, 0, 0)
|
|
radius = 100.0 # Large radius to make sagitta significant
|
|
width = 2.0
|
|
|
|
# Coarse: 1um sagitta
|
|
res_coarse = Bend90.generate(start, radius, width, direction="CCW", sagitta=1.0)
|
|
# Fine: 0.01um (10nm) sagitta
|
|
res_fine = Bend90.generate(start, radius, width, direction="CCW", sagitta=0.01)
|
|
|
|
# Number of segments should be significantly higher for fine
|
|
# Exterior points = (segments + 1) * 2
|
|
pts_coarse = len(res_coarse.geometry[0].exterior.coords)
|
|
pts_fine = len(res_fine.geometry[0].exterior.coords)
|
|
|
|
assert pts_fine > pts_coarse * 2
|
|
|
|
|
|
def test_component_transform_invariance() -> None:
|
|
# Verify that generating at (0,0) then transforming
|
|
# is same as generating at the transformed port.
|
|
start0 = Port(0, 0, 0)
|
|
radius = 10.0
|
|
width = 2.0
|
|
|
|
res0 = Bend90.generate(start0, radius, width, direction="CCW")
|
|
|
|
# Transform: Translate (10, 10) then Rotate 90
|
|
dx, dy = 10.0, 5.0
|
|
angle = 90.0
|
|
|
|
# 1. Transform the generated geometry
|
|
p_end_transformed = rotate_port(translate_port(res0.end_port, dx, dy), angle)
|
|
|
|
# 2. Generate at transformed start
|
|
start_transformed = rotate_port(translate_port(start0, dx, dy), angle)
|
|
res_transformed = Bend90.generate(start_transformed, radius, width, direction="CCW")
|
|
|
|
assert abs(res_transformed.end_port.x - p_end_transformed.x) < 1e-6
|
|
assert abs(res_transformed.end_port.y - p_end_transformed.y) < 1e-6
|
|
assert abs(res_transformed.end_port.orientation - p_end_transformed.orientation) < 1e-6
|