import os import sys import unittest sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) class EdaRouterPinsContractTest(unittest.TestCase): def test_loader_uses_top_level_pins_and_ignores_legacy_ports(self): from mxpic_router.eda_loader import parse_cell_dict spec = parse_cell_dict({ "name": "cell", "pins": [ {"name": "input_io1", "x": 1, "y": 2, "angle": 90, "width": 0.6}, ], "ports": [ {"name": "legacy_port", "x": 9, "y": 9, "angle": 0}, ], }) self.assertIn("input_io1", spec.pins) self.assertNotIn("legacy_port", spec.pins) self.assertEqual(spec.pins["input_io1"].angle, 90.0) def test_loader_accepts_pin_links_with_route_metadata(self): from mxpic_router.eda_loader import parse_cell_dict spec = parse_cell_dict({ "name": "cell_a", "instances": { "inst_a": {"component": "mmi", "x": 0, "y": 0}, "inst_b": {"component": "mmi", "x": 100, "y": 0}, }, "bundles": { "output_bus": { "links": [{ "from": "inst_a:out", "to": "inst_b:in", "xsection": "metal_1", "family": "electrical", "width": 5, "radius": 20, "routing_type": "standard_bend", "points": [{"x": 0, "y": 0}, {"x": 50, "y": 0}], }] } } }) link = spec.bundles["output_bus"].links[0] self.assertEqual(link.src_inst, "inst_a") self.assertEqual(link.src_pin, "out") self.assertEqual(link.dst_inst, "inst_b") self.assertEqual(link.dst_pin, "in") self.assertEqual(link.xsection, "metal_1") self.assertEqual(link.width, 5) self.assertEqual(link.points[1], {"x": 50.0, "y": 0.0}) def test_port_element_creates_named_io_pins_and_inside_route_pins(self): from mxpic_router.builder import _register_element_pins from mxpic_router.eda_loader import parse_cell_dict class FakePlacedPin: def __init__(self, name, x, y, a): self.name = name self.x = x self.y = y self.a = a def move(self, dx, dy, da): return FakePlacedPin(f"{self.name}_in", self.x + dx, self.y + dy, self.a + da) class FakePin: created_names = [] current_cell = None def __init__(self, name, width=None, xs=None): self.name = name self.width = width self.xs = xs self.created_names.append(name) def put(self, x, y, a): placed = FakePlacedPin(self.name, x, y, a) if FakePin.current_cell is not None: FakePin.current_cell.pin[self.name] = placed return placed class FakeCell: put_calls = [] def __init__(self, name=None, instantiate=True): self.name = name self.pin = {} def __enter__(self): FakePin.current_cell = self return self def __exit__(self, exc_type, exc, tb): FakePin.current_cell = None return False def put(self, x, y, a): self.put_calls.append((self.name, x, y, a)) placed = FakeCell(name=f"{self.name}_placed") placed.pin = { name: FakePlacedPin(name, pin.x + x, pin.y + y, pin.a + a) for name, pin in self.pin.items() } return placed class FakeNazca: Pin = FakePin Cell = FakeCell spec = parse_cell_dict({ "name": "cell", "elements": { "input": { "type": "port", "x": 5, "y": 10, "angle": 90, "width": 0.5, "port_number": 2, "pitch": 12, "pins": [ {"name": "input_io1", "role": "io1"}, {"name": "input_io2", "role": "io2"}, ], }, }, }) pin_map = {} _register_element_pins(pin_map, "input", spec.elements["input"], FakeNazca) self.assertEqual(FakePin.created_names, ["input_io1", "input_io1_in", "input_io2", "input_io2_in"]) self.assertEqual(FakeCell.put_calls, [("element_input", 5.0, 10.0, 90.0)]) self.assertEqual(pin_map[("input", "input_io1")].a, 450.0) self.assertEqual(pin_map[("input", "input_io2")].a, 450.0) self.assertEqual((pin_map[("input", "input_io1")].x, pin_map[("input", "input_io1")].y), (5.0, 16.0)) self.assertEqual((pin_map[("input", "input_io2")].x, pin_map[("input", "input_io2")].y), (5.0, 4.0)) def test_anchor_element_defaults_to_named_a_b_pins(self): from mxpic_router.builder import _register_element_pins from mxpic_router.eda_loader import parse_cell_dict class FakePlacedPin: def __init__(self, name, x, y, a): self.name = name self.x = x self.y = y self.a = a class FakePin: current_cell = None def __init__(self, name, width=None, xs=None): self.name = name def put(self, x, y, a): placed = FakePlacedPin(self.name, x, y, a) if FakePin.current_cell is not None: FakePin.current_cell.pin[self.name] = placed return placed class FakeCell: def __init__(self, name=None, instantiate=True): self.name = name self.pin = {} def __enter__(self): FakePin.current_cell = self return self def __exit__(self, exc_type, exc, tb): FakePin.current_cell = None return False def put(self, x, y, a): placed = FakeCell(name=f"{self.name}_placed") placed.pin = { name: FakePlacedPin(name, pin.x + x, pin.y + y, pin.a + a) for name, pin in self.pin.items() } return placed class FakeNazca: Pin = FakePin Cell = FakeCell spec = parse_cell_dict({ "name": "cell", "elements": { "anchor_1": {"type": "anchor", "x": 10, "y": 20, "angle": 0, "port_number": 2, "pitch": 12}, }, }) pin_map = {} _register_element_pins(pin_map, "anchor_1", spec.elements["anchor_1"], FakeNazca) self.assertIn(("anchor_1", "anchor_1_a1"), pin_map) self.assertIn(("anchor_1", "anchor_1_b1"), pin_map) self.assertIn(("anchor_1", "anchor_1_a2"), pin_map) self.assertIn(("anchor_1", "anchor_1_b2"), pin_map) self.assertEqual(pin_map[("anchor_1", "anchor_1_a1")].a, 180.0) self.assertEqual(pin_map[("anchor_1", "anchor_1_b1")].a, 0.0) def test_pdk_metadata_ports_are_registered_as_pins_for_allowed_pdk_roots(self): from mxpic_router.builder import _metadata_pins metadata = {"ports": {"a1": {"x": 0, "y": 0, "a": 180}}} self.assertEqual( _metadata_pins(metadata, r"D:\mxpic\opt_pdk_public\foundries"), metadata["ports"], ) self.assertEqual(_metadata_pins(metadata, r"D:\mxpic\some_project_layout"), {}) def test_eda_rotation_is_converted_to_layout_rotation_for_metadata_pins(self): from mxpic_router.builder import _layout_rotation, _transform_port self.assertEqual(_layout_rotation(90), -90.0) x, y, angle = _transform_port( 39.25, 0.0, 360.0, 1719.3, -2080.5, _layout_rotation(90), ) self.assertAlmostEqual(x, 1719.3) self.assertAlmostEqual(y, -2119.75) self.assertEqual(angle, 270.0) def test_perpendicular_pin_rotations_use_straight_bend_straight_route(self): from mxpic_router.builder import _route_method_name_for_pins class FakePin: def __init__(self, angle): self.a = angle self.assertEqual( _route_method_name_for_pins(FakePin(0), FakePin(90)), "strt_bend_strt_p2p", ) self.assertEqual( _route_method_name_for_pins(FakePin(0), FakePin(270)), "strt_bend_strt_p2p", ) def test_route_spacing_detection_handles_crossing_and_endpoint_exceptions(self): from mxpic_router.builder import ( ROUTE_ENDPOINT_SPACING_IGNORE, ROUTE_MIN_SPACING, _first_spacing_adjusted_route, _orthogonal_elbow, _polyline_spacing, _route_spacing_reference_points, _route_spacing_check_points, _spacing_offsets, _spacing_adjusted_route_points, ) from mxpic_router.eda_loader import LinkSpec class FakePin: def __init__(self, x, y, angle): self.x = x self.y = y self.a = angle self.assertGreaterEqual( _polyline_spacing( [{"x": 0, "y": 0}, {"x": 100, "y": 0}], [{"x": 0, "y": 12}, {"x": 100, "y": 12}], ), ROUTE_MIN_SPACING, ) self.assertLess( _polyline_spacing( [{"x": 0, "y": 0}, {"x": 100, "y": 0}], [{"x": 50, "y": -20}, {"x": 50, "y": 20}], ), ROUTE_MIN_SPACING, ) device_plan = { "link": LinkSpec(src_inst="mmi1", src_pin="o1", dst_inst="mmi2", dst_pin="i1"), "points": [{"x": 0, "y": 0}, {"x": 100, "y": 0}], } nearby_port_plan = { "link": LinkSpec(src_inst="port_1", src_pin="io1", dst_inst="mmi2", dst_pin="i1"), "points": [{"x": 0, "y": 5}, {"x": 100, "y": 5}], } device_check_points = _route_spacing_check_points(device_plan) nearby_port_check_points = _route_spacing_check_points(nearby_port_plan) self.assertEqual(device_check_points[0], {"x": ROUTE_ENDPOINT_SPACING_IGNORE, "y": 0.0}) self.assertEqual(nearby_port_check_points[0], {"x": ROUTE_ENDPOINT_SPACING_IGNORE, "y": 5.0}) self.assertLess(_polyline_spacing(device_check_points, nearby_port_check_points), ROUTE_MIN_SPACING) adjusted = _spacing_adjusted_route_points( LinkSpec(width=0.5, radius=10), FakePin(0, 0, 0), FakePin(100, 0, 180), offset=ROUTE_MIN_SPACING, ) self.assertEqual(adjusted[0], {"x": 0.0, "y": 0.0}) self.assertEqual(adjusted[-1], {"x": 100.0, "y": 0.0}) self.assertEqual(adjusted[2]["y"], ROUTE_MIN_SPACING) self.assertEqual(adjusted[3]["y"], ROUTE_MIN_SPACING) current_plan = { "link": LinkSpec(src_inst="a", src_pin="o1", dst_inst="b", dst_pin="i1"), "pin1": FakePin(0, 5, 0), "pin2": FakePin(100, 5, 180), "points": [{"x": 0, "y": 5}, {"x": 100, "y": 5}], } accepted_below = [{ "check_points": [{"x": 10, "y": 0}, {"x": 90, "y": 0}], }] accepted_above = [{ "check_points": [{"x": 10, "y": 10}, {"x": 90, "y": 10}], }] self.assertEqual(_spacing_offsets(current_plan, accepted_below)[0], ROUTE_MIN_SPACING / 2.0) self.assertEqual(_spacing_offsets(current_plan, accepted_above)[0], -ROUTE_MIN_SPACING / 2.0) adjusted_from_close_route = _first_spacing_adjusted_route(current_plan, accepted_below) self.assertIsNotNone(adjusted_from_close_route) self.assertEqual(adjusted_from_close_route["points"][2]["y"], 10.0) elbow = _orthogonal_elbow({"x": 0, "y": 0}, 0, {"x": 20, "y": 30}, 270) self.assertEqual(elbow, {"x": 20.0, "y": 0.0}) reference = _route_spacing_reference_points( LinkSpec(), FakePin(0, 0, 0), FakePin(20, 30, 270), ) self.assertEqual(reference, [{"x": 0.0, "y": 0.0}, {"x": 20.0, "y": 0.0}, {"x": 20.0, "y": 30.0}]) def test_automatic_p2p_spacing_detection_does_not_change_geometry_by_default(self): from mxpic_router.builder import _route_bundle_links from mxpic_router.eda_loader import LinkSpec class FakePin: def __init__(self, x, y, angle): self.x = x self.y = y self.a = angle class FakeRoute: calls = [] def __init__(self, **kwargs): pass def sbend_p2p(self, **kwargs): self.calls.append(("sbend_p2p", kwargs)) return self def strt_p2p(self, **kwargs): self.calls.append(("strt_p2p", kwargs)) return self def strt_bend_strt_p2p(self, **kwargs): self.calls.append(("strt_bend_strt_p2p", kwargs)) return self def put(self): self.calls.append(("put", {})) FakeRoute.calls = [] links = [ LinkSpec(src_inst="mmi1", src_pin="b1", dst_inst="port_1", dst_pin="io1"), LinkSpec(src_inst="mmi2", src_pin="b1", dst_inst="port_1", dst_pin="io2"), ] pin_map = { ("mmi1", "b1"): FakePin(0, 0, 0), ("port_1", "io1"): FakePin(100, 0, 180), ("mmi2", "b1"): FakePin(0, 5, 0), ("port_1", "io2"): FakePin(100, 5, 180), } warnings = [] _route_bundle_links(links, pin_map, FakeRoute, warnings) self.assertEqual([call[0] for call in FakeRoute.calls], ["sbend_p2p", "put", "sbend_p2p", "put"]) self.assertTrue(any("Detected route spacing below 10um" in warning for warning in warnings)) self.assertEqual(FakeRoute.calls[0][1]["Lstart"], 10.5) self.assertEqual(FakeRoute.calls[2][1]["Lstart"], 10.5) call_names = [call[0] for call in FakeRoute.calls] self.assertNotIn("strt_bend_strt_p2p", call_names) self.assertNotIn("strt_p2p", call_names) def test_sbend_lstart_spacing_uses_whole_bundle_inner_outer_order(self): from mxpic_router.builder import _route_bundle_links from mxpic_router.eda_loader import LinkSpec class FakePin: def __init__(self, x, y, angle): self.x = x self.y = y self.a = angle class FakeRoute: calls = [] def __init__(self, **kwargs): pass def sbend_p2p(self, **kwargs): self.calls.append(("sbend_p2p", kwargs)) return self def put(self): self.calls.append(("put", {})) FakeRoute.calls = [] links = [ LinkSpec(src_inst="mmi_top", src_pin="b1", dst_inst="port_top", dst_pin="io1", width=0.5, radius=10), LinkSpec(src_inst="mmi_top", src_pin="b2", dst_inst="port_mid_top", dst_pin="io1", width=0.5, radius=10), LinkSpec(src_inst="mmi_bot", src_pin="b1", dst_inst="port_mid_bot", dst_pin="io1", width=0.5, radius=10), LinkSpec(src_inst="mmi_bot", src_pin="b2", dst_inst="port_bot", dst_pin="io1", width=0.5, radius=10), ] pin_map = { ("mmi_top", "b1"): FakePin(0, 12, 0), ("port_top", "io1"): FakePin(100, 12, 180), ("mmi_top", "b2"): FakePin(0, 4, 0), ("port_mid_top", "io1"): FakePin(100, 4, 180), ("mmi_bot", "b1"): FakePin(0, -4, 0), ("port_mid_bot", "io1"): FakePin(100, -4, 180), ("mmi_bot", "b2"): FakePin(0, -12, 0), ("port_bot", "io1"): FakePin(100, -12, 180), } _route_bundle_links(links, pin_map, FakeRoute, []) lstarts = [call[1]["Lstart"] for call in FakeRoute.calls if call[0] == "sbend_p2p"] self.assertEqual(lstarts, [21.0, 10.5, 10.5, 21.0]) def test_sbend_lstart_spacing_uses_monotonic_order_for_same_direction_fanout(self): from mxpic_router.builder import _route_bundle_links from mxpic_router.eda_loader import LinkSpec class FakePin: def __init__(self, x, y, angle): self.x = x self.y = y self.a = angle class FakeRoute: calls = [] def __init__(self, **kwargs): pass def sbend_p2p(self, **kwargs): self.calls.append(("sbend_p2p", kwargs)) return self def put(self): self.calls.append(("put", {})) FakeRoute.calls = [] links = [ LinkSpec(src_inst="mmi1", src_pin="b1", dst_inst="port1", dst_pin="io1", width=0.5, radius=10), LinkSpec(src_inst="mmi1", src_pin="b2", dst_inst="port2", dst_pin="io1", width=0.5, radius=10), LinkSpec(src_inst="mmi2", src_pin="b1", dst_inst="port3", dst_pin="io1", width=0.5, radius=10), LinkSpec(src_inst="mmi2", src_pin="b2", dst_inst="port4", dst_pin="io1", width=0.5, radius=10), ] pin_map = { ("mmi1", "b1"): FakePin(0, 40, 0), ("port1", "io1"): FakePin(100, 20, 180), ("mmi1", "b2"): FakePin(0, 32, 0), ("port2", "io1"): FakePin(100, 12, 180), ("mmi2", "b1"): FakePin(0, 24, 0), ("port3", "io1"): FakePin(100, 4, 180), ("mmi2", "b2"): FakePin(0, 16, 0), ("port4", "io1"): FakePin(100, -4, 180), } _route_bundle_links(links, pin_map, FakeRoute, []) lstarts = [call[1]["Lstart"] for call in FakeRoute.calls if call[0] == "sbend_p2p"] self.assertEqual(lstarts, [42.0, 31.5, 21.0, 10.5]) def test_sbend_lstart_step_includes_route_width(self): from mxpic_router.builder import _route_bundle_links from mxpic_router.eda_loader import LinkSpec class FakePin: def __init__(self, x, y, angle): self.x = x self.y = y self.a = angle class FakeRoute: calls = [] def __init__(self, **kwargs): pass def sbend_p2p(self, **kwargs): self.calls.append(("sbend_p2p", kwargs)) return self def put(self): self.calls.append(("put", {})) FakeRoute.calls = [] links = [ LinkSpec(src_inst="r1", src_pin="b1", dst_inst="p1", dst_pin="io1", width=40, radius=10), LinkSpec(src_inst="r2", src_pin="b1", dst_inst="p2", dst_pin="io1", width=40, radius=10), ] pin_map = { ("r1", "b1"): FakePin(0, 0, 0), ("p1", "io1"): FakePin(100, 0, 180), ("r2", "b1"): FakePin(0, 100, 0), ("p2", "io1"): FakePin(100, 100, 180), } _route_bundle_links(links, pin_map, FakeRoute, []) lstarts = [call[1]["Lstart"] for call in FakeRoute.calls if call[0] == "sbend_p2p"] self.assertEqual(lstarts, [50.0, 50.0]) def test_route_backend_falls_back_to_nazca_interconnect_when_forge_is_absent(self): import mxpic_router.builder as builder class FakeInterconnect: calls = [] def __init__(self, radius=None, width=None, xs=None, PCB=False): self.radius = radius self.width = width self.xs = xs self.PCB = PCB def strt_p2p(self, **kwargs): self.calls.append(("strt_p2p", kwargs)) return "straight" def sbend_p2p(self, **kwargs): self.calls.append(("sbend_p2p", kwargs)) return "sbend" def ubend_p2p(self, **kwargs): self.calls.append(("ubend_p2p", kwargs)) return "ubend" def bend_strt_bend_p2p(self, **kwargs): self.calls.append(("bend_strt_bend_p2p", kwargs)) return "bend" def strt_bend_strt_p2p(self, **kwargs): self.calls.append(("strt_bend_strt_p2p", kwargs)) return "straight_bend_straight" class FakeInterconnects: Interconnect = FakeInterconnect class FakeNazca: interconnects = FakeInterconnects original_import = builder._import_mxpic_forge_route original_create = builder._NazcaInterconnectRoute._create_interconnect try: builder._import_mxpic_forge_route = lambda: (_ for _ in ()).throw(ImportError("no forge")) builder._NazcaInterconnectRoute._create_interconnect = staticmethod( lambda **kwargs: FakeInterconnect(**kwargs) ) route_backend = builder._import_route_backend(FakeNazca) route = route_backend(radius=20, width=0.7, xs="strip", PCB=False) self.assertEqual(route.backend_name, "nazca Interconnect") self.assertEqual(route.strt_p2p(pin1="a", pin2="b", arrow=False), "straight") self.assertEqual( route.strt_bend_strt_p2p(pin1="a", pin2="b", radius=12, arrow=False), "straight_bend_straight", ) self.assertEqual(route.bend_p2p(pin1="a", pin2="b", radius=10, arrow=False), "bend") self.assertEqual(FakeInterconnect.calls[0][0], "strt_p2p") self.assertEqual(FakeInterconnect.calls[0][1]["width"], 0.7) self.assertEqual(FakeInterconnect.calls[1][0], "strt_bend_strt_p2p") self.assertEqual(FakeInterconnect.calls[1][1]["radius"], 12) self.assertEqual(FakeInterconnect.calls[2][0], "bend_strt_bend_p2p") self.assertEqual(FakeInterconnect.calls[2][1]["radius"], 10) finally: builder._import_mxpic_forge_route = original_import builder._NazcaInterconnectRoute._create_interconnect = original_create if __name__ == "__main__": unittest.main()