"""Regression: PCB software false positives (not HubAudio copper).""" from __future__ import annotations import re from pathlib import Path from backend.periscopex.bom_pcb_check import check_bom_pcb_datasheet from backend.periscopex.emi_check import check_emi from backend.periscopex.models import ( AbsMaxRating, Component, ComponentConstraints, ComponentType, DesignGraph, InductorSpecs, LayoutFootprint, LayoutGraph, LayoutPad, LayoutSegment, LayoutZone, Net, NetType, PackageInfo, Pin, PinConnection, ResistorSpecs, SimpleComponentSpecs, ValueDecoder, ) from backend.periscopex.parsers_kicad_pcb import parse_kicad_pcb from backend.periscopex.pcb_checks import unrouted_pad_nets from backend.periscopex.pcb_power_thermal import ( check_pcb_kelvin, check_pcb_power_traces, check_pcb_via_current, ) from backend.periscopex.pi_check import check_power_integrity from backend.periscopex.resolve_passives import decode_value from backend.services.passive_from_mpn import specs_from_mpn from backend.services.passive_from_value import specs_from_bom_value _DRC = Path(__file__).parent / "fixtures" / "HubAudio-DRC.rpt" _HUB_PCB = Path( "/Users/michelebigi/Development/HubAudio/hardware/kicad/HubAudio/HubAudio.kicad_pcb" ) _KICAD10 = """(kicad_pcb (version 20260206) (generator pcbnew) (net "GND") (net "+3V3") (footprint "R_0603" (layer "F.Cu") (at 10 10 0) (property "Reference" "R1" (at 0 0 0) (effects (font (size 1 1)))) (pad "1" smd rect (at -0.5 0) (size 0.8 0.9) (layers "F.Cu") (net "+3V3")) (pad "2" smd rect (at 0.5 0) (size 0.8 0.9) (layers "F.Cu") (net "GND")) ) (segment (start 10 10) (end 12 10) (width 0.2) (layer "F.Cu") (net "+3V3")) (via (at 11 10) (size 0.8) (drill 0.4) (layers "F.Cu" "B.Cu") (net "GND")) ) """ def test_kicad10_name_only_nets_fill_index(tmp_path: Path): p = tmp_path / "k10.kicad_pcb" p.write_text(_KICAD10) g = parse_kicad_pcb(p) assert "GND" in g.nets and "+3V3" in g.nets assert g.footprints["R1"].pads[0].net == "+3V3" assert g.segments[0].net == "+3V3" assert g.vias[0].net == "GND" def test_lay004_zone_counts_as_copper(): layout = LayoutGraph( footprints={ "C1": LayoutFootprint( reference="C1", x=0, y=0, layer="F.Cu", pads=[ LayoutPad(number="1", x=0, y=0, net="GND"), LayoutPad(number="2", x=1, y=0, net="USB_DP"), ], ), }, zones=[ LayoutZone( net="GND", layer="In1.Cu", outlines=[[(0, 0), (10, 0), (10, 10), (0, 10)]], ), ], ) missing = unrouted_pad_nets(layout) assert missing == ["USB_DP"] def test_lay004_skips_unconnected_nc_and_matches_sheet_prefix(): layout = LayoutGraph( footprints={ "U1": LayoutFootprint( reference="U1", x=0, y=0, layer="F.Cu", pads=[ LayoutPad(number="1", x=0, y=0, net="/Codec/LINE_OUT_R"), LayoutPad(number="2", x=1, y=0, net="unconnected-(U1-NC-Pad2)"), ], ), }, segments=[ LayoutSegment( start=(0, 0), end=(2, 0), width=0.2, layer="F.Cu", net="Codec/LINE_OUT_R", ), ], ) assert unrouted_pad_nets(layout) == [] def test_drc_unconnected_items_count(): text = _DRC.read_text(encoding="utf-8", errors="replace") m = re.search(r"Found (\d+) unconnected pads", text) assert m and int(m.group(1)) == 30 n = len(re.findall(r"^\[unconnected_items\]", text, re.M)) assert n == 30 def test_hubaudio_pcb_lay004_vs_drc_order_of_magnitude(): if not _HUB_PCB.is_file(): return layout = parse_kicad_pcb(_HUB_PCB) assert layout.nets, "KiCad 10 name-only nets must fill LayoutGraph.nets" missing = unrouted_pad_nets(layout) assert len(missing) <= 30, missing[:20] assert len(missing) < 139 def test_ep_split_pads_not_bom_011(): cons = ComponentConstraints( mpn="SW", package_info=PackageInfo(base_family="TI", package="SON-8-EP", pin_count=8), pintable=[Pin(number=str(i), name=f"P{i}") for i in range(1, 9)], absolute_maximum_ratings=[], rules=[], ) pads = [LayoutPad(number=str(i), x=0, y=0, net="GND") for i in range(1, 9)] pads += [ LayoutPad(number="9", x=0, y=0, net="GND", pinfunction="EP"), LayoutPad(number="9_1", x=0.2, y=0, net="GND"), LayoutPad(number="9_2", x=0.4, y=0, net="GND"), ] graph = DesignGraph( components={ "U1": Component( reference="U1", value="SW", footprint="SON-8", component_type=ComponentType.IC, mpn="SW", pins={str(i): "GND" for i in range(1, 9)}, ), }, nets={}, ) layout = LayoutGraph( footprints={"U1": LayoutFootprint(reference="U1", x=0, y=0, pads=pads)}, ) ids = {f.rule_id for f in check_bom_pcb_datasheet(graph, {"SW": cons}, layout)} assert "PE-BOM-011" not in ids def test_vddcr_absmax_does_not_bind_3v3_io(): cons = ComponentConstraints( mpn="PHY", package_info=PackageInfo(base_family="SMS", package="QFN-24", pin_count=24), pintable=[ Pin(number="6", name="VDDCR"), Pin(number="8", name="VDDIO"), ], absolute_maximum_ratings=[ AbsMaxRating( parameter="Digital Core Supply Voltage (VDDCR)", min=None, max=1.5, unit="V", source_page=52, ), AbsMaxRating( parameter="Positive voltage on XTAL2, with respect to ground", min=None, max=2.5, unit="V", source_page=52, ), AbsMaxRating( parameter="Ground voltage differences VSS to VSS (thermal pad)", min=None, max=0.3, unit="V", source_page=7, ), ], rules=[], ) graph = DesignGraph( components={ "U19": Component( reference="U19", value="PHY", footprint="QFN-24", component_type=ComponentType.IC, mpn="PHY", pins={"6": "Net-VDDCR", "8": "3V3_ETHERNET"}, ), }, nets={ "3V3_ETHERNET": Net( name="3V3_ETHERNET", net_type=NetType.POWER, voltage=3.3, pins=[PinConnection(component_ref="U19", pin_number="8")], ), "Net-VDDCR": Net( name="Net-VDDCR", net_type=NetType.POWER, voltage=1.2, pins=[PinConnection(component_ref="U19", pin_number="6")], ), }, ) findings = check_bom_pcb_datasheet(graph, {"PHY": cons}, None) assert all(f.rule_id != "PE-BOM-012" for f in findings) def test_kelvin_ignores_crs_and_in_plus(): cons = ComponentConstraints( mpn="PHY", pintable=[ Pin(number="11", name="CRS_DV/MODE2", description="carrier sense"), Pin(number="10", name="IN+", description="differential input"), ], absolute_maximum_ratings=[], rules=[], ) graph = DesignGraph( components={ "U19": Component( reference="U19", value="PHY", footprint="", component_type=ComponentType.IC, mpn="PHY", pins={"11": "ETH_CRS_DV", "10": "VSYS"}, ), "R1": Component( reference="R1", value="10k", footprint="", component_type=ComponentType.RESISTOR, mpn="", pins={"1": "ETH_CRS_DV", "2": "VSYS"}, ), "U4": Component( reference="U4", value="AMP", footprint="", component_type=ComponentType.IC, mpn="X", pins={"1": "ETH_CRS_DV", "2": "VSYS"}, ), }, nets={}, ) assert check_pcb_kelvin(graph, {"PHY": cons}) == [] def test_emi_and_pi_skip_unconnected_nets(): cons = ComponentConstraints( mpn="MOD", pintable=[ Pin(number="38", name="USB_DN"), Pin(number="23", name="NC"), Pin(number="31", name="VDD_USB"), ], absolute_maximum_ratings=[], rules=[], layout_rules=[{ "kind": "emi", "note": "antenna port 50 ohm ferrite", "source_page": 12, }], ) graph = DesignGraph( components={ "U38": Component( reference="U38", value="BT", footprint="", component_type=ComponentType.IC, mpn="MOD", pins={ "38": "unconnected-(U38-USB_DN-Pad38)", "31": "unconnected-(U38-VDD_USB-Pad31)", "23": "unconnected-(U12-NC-Pad23)", }, ), }, nets={}, ) assert check_emi(graph, {"MOD": cons}, None) == [] assert check_power_integrity(graph, {"MOD": cons}) == [] def test_lqw_decoder_and_unknown_skip(): dec = ValueDecoder( type="murata_lqw_inductance", base_unit="nH", output_unit="H", ) assert abs(decode_value("18N", dec) - 18e-9) < 1e-15 model = specs_from_mpn("LQW18AN18NJ00D") assert model is not None assert abs(model.specs.value_henries - 18e-9) < 1e-15 bad = ValueDecoder(type="nope", base_unit="H", output_unit="H") try: decode_value("18N", bad) raise AssertionError("expected unknown decoder") except ValueError as exc: assert "Unknown decoder type" in str(exc) def test_fb1_blm_is_bead_not_dcr_resistor(): model = specs_from_mpn("BLM18BB600SN1D") assert model is not None assert model.specs.component_subtype == "passive.ferrite_bead" assert model.specs.impedance_ohm == 60.0 dcr = specs_from_bom_value("BLM18BB600SN1D", "0.25 ohm", "FB") assert dcr is not None assert dcr.specs.component_subtype == "passive.ferrite_bead" assert isinstance(dcr.specs, InductorSpecs) assert not isinstance(dcr.specs, ResistorSpecs) assert dcr.specs.impedance_ohm == 60.0 assert getattr(dcr.specs, "value_ohms", None) is None def test_via_current_skips_without_i_load(): from backend.periscopex.models import LayoutStackup cons = ComponentConstraints( mpn="LDO1", pintable=[Pin(number="1", name="VIN"), Pin(number="2", name="VOUT")], absolute_maximum_ratings=[], rules=[], ) graph = DesignGraph( components={ "U1": Component( reference="U1", value="LDO", footprint="", component_type=ComponentType.IC, mpn="LDO1", pins={"1": "VIN", "2": "VOUT"}, specs=SimpleComponentSpecs(specs_type="discrete", values={}), ), }, nets={ "VOUT": Net(name="VOUT", net_type=NetType.POWER, pins=[]), }, ) layout = LayoutGraph( stackup=LayoutStackup( copper_layers=["F.Cu"], dielectrics=[], copper_thickness_mm=0.035, ), footprints={"U1": LayoutFootprint(reference="U1", x=0, y=0, layer="F.Cu")}, segments=[ LayoutSegment(start=(0, 0), end=(5, 0), width=0.5, layer="F.Cu", net="VOUT"), ], ) findings = check_pcb_via_current(graph, {"LDO1": cons}, layout) assert findings == [] pwr = check_pcb_power_traces(graph, {"LDO1": cons}, layout) assert pwr == [] def test_thinking_reasoning_content_echo_still_present(): from pathlib import Path src = ( Path(__file__).resolve().parents[1] / "periscope" / "src" / "backend" / "services" / "llm" / "deepseek_provider.py" ) text = src.read_text(encoding="utf-8") assert "reasoning_content" in text assert "asst[\"reasoning_content\"] = reasoning" in text or 'asst["reasoning_content"] = reasoning' in text