ecadlib 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. ecadlib/__init__.py +3 -0
  2. ecadlib/__main__.py +5 -0
  3. ecadlib/a365_sync.py +637 -0
  4. ecadlib/agent.py +1407 -0
  5. ecadlib/agent_queue.py +780 -0
  6. ecadlib/agent_watch.py +732 -0
  7. ecadlib/altium365.py +385 -0
  8. ecadlib/altium_dblib_sync.py +530 -0
  9. ecadlib/altium_export.py +209 -0
  10. ecadlib/altium_forms.py +768 -0
  11. ecadlib/altium_lib.py +1089 -0
  12. ecadlib/altium_menu.py +1751 -0
  13. ecadlib/altium_pkg.py +396 -0
  14. ecadlib/altium_project.py +1095 -0
  15. ecadlib/app.py +1458 -0
  16. ecadlib/approvals.py +395 -0
  17. ecadlib/assets/demo_bom.csv +8 -0
  18. ecadlib/assets/kicad_action_plugin.py +607 -0
  19. ecadlib/assets/kicad_reread.py +60 -0
  20. ecadlib/assets/pcblib_template.json.gz +0 -0
  21. ecadlib/bom_inputs.py +339 -0
  22. ecadlib/bom_triage.py +519 -0
  23. ecadlib/cad_model.py +528 -0
  24. ecadlib/cfb.py +171 -0
  25. ecadlib/cli.py +185 -0
  26. ecadlib/clihelp.py +149 -0
  27. ecadlib/cliutil.py +119 -0
  28. ecadlib/completion.py +173 -0
  29. ecadlib/costs.py +276 -0
  30. ecadlib/crosscheck.py +412 -0
  31. ecadlib/datasheet.py +2168 -0
  32. ecadlib/dedupe.py +572 -0
  33. ecadlib/detect.py +170 -0
  34. ecadlib/doctor.py +450 -0
  35. ecadlib/ds_route.py +541 -0
  36. ecadlib/eagle_export.py +1409 -0
  37. ecadlib/easy_setup.py +338 -0
  38. ecadlib/easyeda_convert.py +386 -0
  39. ecadlib/easyeda_pro_export.py +972 -0
  40. ecadlib/eval/__init__.py +0 -0
  41. ecadlib/eval/baselines/kicad-v1.json +404 -0
  42. ecadlib/eval/build_set.py +299 -0
  43. ecadlib/eval/cli.py +119 -0
  44. ecadlib/eval/gate.py +358 -0
  45. ecadlib/eval/golden/demo.txt +379 -0
  46. ecadlib/eval/golden/specs.txt +3120 -0
  47. ecadlib/eval/golden/testpack.txt +463 -0
  48. ecadlib/eval/golden.py +446 -0
  49. ecadlib/eval/replay/kicad-v1.json.gz +0 -0
  50. ecadlib/eval/runner.py +456 -0
  51. ecadlib/eval/score.py +179 -0
  52. ecadlib/eval/sets/kicad-v1.json +630 -0
  53. ecadlib/firstrun.py +400 -0
  54. ecadlib/footprint_adv.py +1006 -0
  55. ecadlib/footprint_bench.py +218 -0
  56. ecadlib/footprint_check.py +562 -0
  57. ecadlib/footprint_gen.py +582 -0
  58. ecadlib/footprint_rules.py +1092 -0
  59. ecadlib/forge.py +267 -0
  60. ecadlib/fp_share.py +95 -0
  61. ecadlib/fp_tidy.py +389 -0
  62. ecadlib/fsutil.py +62 -0
  63. ecadlib/house_style.py +1051 -0
  64. ecadlib/httpcache.py +124 -0
  65. ecadlib/hub.py +1490 -0
  66. ecadlib/hub_copy.py +161 -0
  67. ecadlib/hub_tools.py +79 -0
  68. ecadlib/kicad_dblib.py +257 -0
  69. ecadlib/kicad_export.py +837 -0
  70. ecadlib/kicad_import.py +913 -0
  71. ecadlib/kicad_index.py +160 -0
  72. ecadlib/kicad_menu.py +193 -0
  73. ecadlib/kicad_pcm.py +475 -0
  74. ecadlib/launcher.py +156 -0
  75. ecadlib/librarian.py +963 -0
  76. ecadlib/lifecycle.py +691 -0
  77. ecadlib/llm/__init__.py +276 -0
  78. ecadlib/llm/cli.py +70 -0
  79. ecadlib/llm/openai_compat.py +214 -0
  80. ecadlib/llm/profiles.py +245 -0
  81. ecadlib/llm/rails.py +570 -0
  82. ecadlib/llm/run.py +126 -0
  83. ecadlib/llm/sampling.py +49 -0
  84. ecadlib/mcp_server.py +723 -0
  85. ecadlib/messages.py +419 -0
  86. ecadlib/migrate.py +407 -0
  87. ecadlib/notify.py +329 -0
  88. ecadlib/package_schema.json +1238 -0
  89. ecadlib/package_specs/CP_EIA-3216-18_Kemet-A.json +27 -0
  90. ecadlib/package_specs/C_0402_1005Metric.json +22 -0
  91. ecadlib/package_specs/C_0805_2012Metric.json +22 -0
  92. ecadlib/package_specs/DFN-8-1EP_3x3mm_P0.5mm_EP1.66x2.38mm.json +40 -0
  93. ecadlib/package_specs/D_SMA.json +27 -0
  94. ecadlib/package_specs/D_SMB.json +27 -0
  95. ecadlib/package_specs/D_SOD-123.json +43 -0
  96. ecadlib/package_specs/D_SOD-323.json +43 -0
  97. ecadlib/package_specs/LED_0603_1608Metric.json +23 -0
  98. ecadlib/package_specs/LED_0805_2012Metric.json +23 -0
  99. ecadlib/package_specs/LQFP-48_7x7mm_P0.5mm.json +31 -0
  100. ecadlib/package_specs/L_0805_2012Metric.json +22 -0
  101. ecadlib/package_specs/MSOP-8_3x3mm_P0.65mm.json +31 -0
  102. ecadlib/package_specs/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm.json +39 -0
  103. ecadlib/package_specs/QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm.json +37 -0
  104. ecadlib/package_specs/R_0402_1005Metric.json +22 -0
  105. ecadlib/package_specs/R_0603_1608Metric.json +22 -0
  106. ecadlib/package_specs/R_0805_2012Metric.json +22 -0
  107. ecadlib/package_specs/R_1206_3216Metric.json +22 -0
  108. ecadlib/package_specs/SOIC-16_3.9x9.9mm_P1.27mm.json +32 -0
  109. ecadlib/package_specs/SOIC-8_3.9x4.9mm_P1.27mm.json +32 -0
  110. ecadlib/package_specs/SOT-223-3_TabPin2.json +56 -0
  111. ecadlib/package_specs/SOT-23-5.json +57 -0
  112. ecadlib/package_specs/SOT-23-6.json +62 -0
  113. ecadlib/package_specs/SOT-23.json +48 -0
  114. ecadlib/package_specs/TSSOP-16_4.4x5mm_P0.65mm.json +31 -0
  115. ecadlib/package_specs/VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm.json +39 -0
  116. ecadlib/package_specs/advanced/BGA-100_11.0x11.0mm_Layout10x10_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  117. ecadlib/package_specs/advanced/BGA-121_9.0x9.0mm_Layout11x11_P0.8mm_Ball0.4mm_Pad0.35mm_NSMD.json +26 -0
  118. ecadlib/package_specs/advanced/BGA-144_7.0x7.0mm_Layout13x13_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +29 -0
  119. ecadlib/package_specs/advanced/BGA-169_11.0x11.0mm_Layout13x13_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +23 -0
  120. ecadlib/package_specs/advanced/BGA-256_11.0x11.0mm_Layout20x20_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +26 -0
  121. ecadlib/package_specs/advanced/BGA-256_17.0x17.0mm_Layout16x16_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  122. ecadlib/package_specs/advanced/BGA-324_15.0x15.0mm_Layout18x18_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
  123. ecadlib/package_specs/advanced/BGA-36_3.396x3.466mm_Layout6x6_P0.4mm_Ball0.25mm_Pad0.2mm_NSMD.json +24 -0
  124. ecadlib/package_specs/advanced/BGA-672_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD.json +32 -0
  125. ecadlib/package_specs/advanced/BGA-68_5.0x5.0mm_Layout9x9_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +30 -0
  126. ecadlib/package_specs/advanced/BGA-90_8.0x13.0mm_Layout2x3x15_P0.8mm.json +26 -0
  127. ecadlib/package_specs/advanced/DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias.json +60 -0
  128. ecadlib/package_specs/advanced/Example_Custom_LPad_Slot.json +24 -0
  129. ecadlib/package_specs/advanced/JST_GH_SM04B-GHS-TB_1x04-1MP_P1.25mm_Horizontal.json +49 -0
  130. ecadlib/package_specs/advanced/JST_PH_B4B-PH-K_1x04_P2.00mm_Vertical.json +33 -0
  131. ecadlib/package_specs/advanced/JST_PH_S4B-PH-SM4-TB_1x04-1MP_P2.00mm_Horizontal.json +55 -0
  132. ecadlib/package_specs/advanced/JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal.json +49 -0
  133. ecadlib/package_specs/advanced/JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical.json +34 -0
  134. ecadlib/package_specs/advanced/JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical.json +34 -0
  135. ecadlib/package_specs/advanced/JST_XH_B6B-XH-A_1x06_P2.50mm_Vertical.json +33 -0
  136. ecadlib/package_specs/advanced/LFBGA-144_10x10mm_Layout12x12_P0.8mm.json +23 -0
  137. ecadlib/package_specs/advanced/Molex_KK-254_AE-6410-04A_1x04_P2.54mm_Vertical.json +34 -0
  138. ecadlib/package_specs/advanced/Molex_PicoBlade_53398-0471_1x04-1MP_P1.25mm_Vertical.json +54 -0
  139. ecadlib/package_specs/advanced/PinHeader_1x05_P1.00mm_Vertical_SMD_Pin1Left.json +14 -0
  140. ecadlib/package_specs/advanced/PinHeader_1x05_P1.27mm_Vertical_SMD_Pin1Left.json +14 -0
  141. ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical.json +12 -0
  142. ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical_SMD_Pin1Left.json +14 -0
  143. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical.json +12 -0
  144. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Left.json +25 -0
  145. ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Right.json +14 -0
  146. ecadlib/package_specs/advanced/PinHeader_1x10_P1.00mm_Vertical.json +15 -0
  147. ecadlib/package_specs/advanced/PinHeader_2x05_P1.00mm_Vertical_SMD.json +13 -0
  148. ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical.json +18 -0
  149. ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical_SMD.json +24 -0
  150. ecadlib/package_specs/advanced/PinHeader_2x05_P2.00mm_Vertical_SMD.json +13 -0
  151. ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical.json +12 -0
  152. ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical_SMD.json +24 -0
  153. ecadlib/package_specs/advanced/PinHeader_2x20_P2.54mm_Vertical.json +12 -0
  154. ecadlib/package_specs/advanced/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm_ThermalVias.json +87 -0
  155. ecadlib/package_specs/advanced/UFBGA-132_7x7mm_Layout12x12_P0.5mm.json +43 -0
  156. ecadlib/package_specs/advanced/USB_C_Receptacle_HRO_TYPE-C-31-M-12.json +218 -0
  157. ecadlib/package_specs/standard.json.gz +0 -0
  158. ecadlib/parity.py +1204 -0
  159. ecadlib/parts_export.py +154 -0
  160. ecadlib/partview.py +215 -0
  161. ecadlib/plugins.py +231 -0
  162. ecadlib/providers.py +226 -0
  163. ecadlib/rebuild.py +471 -0
  164. ecadlib/release_check.py +437 -0
  165. ecadlib/reports.py +274 -0
  166. ecadlib/resolve.py +556 -0
  167. ecadlib/review_cli.py +292 -0
  168. ecadlib/rules.py +979 -0
  169. ecadlib/rules_infer.py +1205 -0
  170. ecadlib/sexpr.py +149 -0
  171. ecadlib/sheetsafe.py +65 -0
  172. ecadlib/sim_models.py +1087 -0
  173. ecadlib/sourcing.py +1161 -0
  174. ecadlib/stdparts.py +755 -0
  175. ecadlib/step_gen.py +929 -0
  176. ecadlib/symbol_layout.py +798 -0
  177. ecadlib/team.py +558 -0
  178. ecadlib/team_setup.py +288 -0
  179. ecadlib/term.py +420 -0
  180. ecadlib/term_views.py +64 -0
  181. ecadlib/testpack.py +334 -0
  182. ecadlib/ui/__init__.py +1 -0
  183. ecadlib/ui/__main__.py +3 -0
  184. ecadlib/ui/server.py +1029 -0
  185. ecadlib/ui/static/hairline.js +2 -0
  186. ecadlib/ui/static/index.html +2098 -0
  187. ecadlib/ui/static/setup.html +294 -0
  188. ecadlib/upkeep.py +717 -0
  189. ecadlib/upstream.py +573 -0
  190. ecadlib/userconfig.py +257 -0
  191. ecadlib/visual_check.py +1603 -0
  192. ecadlib/ws_status.py +203 -0
  193. ecadlib-0.3.0.dist-info/METADATA +2245 -0
  194. ecadlib-0.3.0.dist-info/RECORD +199 -0
  195. ecadlib-0.3.0.dist-info/WHEEL +5 -0
  196. ecadlib-0.3.0.dist-info/entry_points.txt +2 -0
  197. ecadlib-0.3.0.dist-info/licenses/LICENSE +21 -0
  198. ecadlib-0.3.0.dist-info/licenses/NOTICE +53 -0
  199. ecadlib-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,299 @@
1
+ """Build the accuracy test set from a checkout of KiCad's official libraries (ground truth).
2
+
3
+ ecadlib eval build-set --symbols kicad-symbols --footprints kicad-footprints [-o ecadlib/eval/sets/kicad-v1.json]
4
+
5
+ Cases come from three places, each recorded in the case's `from`:
6
+ kicad-symbol a symbol in kicad-symbols whose name is a real MPN (no KiCad `x` wildcards, no `_` variants), with a
7
+ fixed Footprint property and a Datasheet link: MPN = symbol name, footprint = that property, pins =
8
+ the symbol's pin numbers and names, manufacturer = the datasheet's domain (a fixed table; distributor
9
+ and archive links give none).
10
+ kicad-footprint a connector footprint whose name carries the maker's part number (JST B4B-XH-A, Molex 53398-0471,
11
+ HRO TYPE-C-31-M-12 ...): footprint = that file, datasheet = the link in its descr, pins = its
12
+ numbered pads (a generic connector symbol: numbers only, no names).
13
+ curated chip passives by the makers' ordering codes (Yageo RC0603FR-..., Murata GRM188..., Samsung CL10...,
14
+ KEMET C0603C..., Panasonic ERJ-3E...): the size code fixes the KiCad chip footprint. Series datasheet
15
+ links are given only where the maker publishes one series PDF (they were not fetched from here).
16
+ Selection is deterministic (sorted, then evenly spread over each family with at most PER_FOOTPRINT cases per
17
+ footprint) so the same checkout gives the same set. The set keeps only numbers from KiCad's files (pad centres,
18
+ sizes, drills, courtyard box; pin numbers and names): facts, not the drawings. KiCad libraries: CC-BY-SA 4.0 with
19
+ the KiCad libraries exception, https://gitlab.com/kicad/libraries."""
20
+ import argparse, datetime, json, re, subprocess, sys
21
+ from pathlib import Path
22
+
23
+ from .. import footprint_bench as FB, kicad_import as K, sexpr
24
+
25
+ # family -> how many cases to keep; classify() maps a KiCad footprint to one of these
26
+ QUOTA = {"chip": 36, "sot": 40, "soic": 30, "tssop": 30, "qfn": 40, "qfp": 25, "bga": 25, "diode": 25, "power": 15,
27
+ "tht": 30, "connector": 35}
28
+ PER_FOOTPRINT = 2
29
+ MAKERS = {"st.com": "STMicroelectronics", "ti.com": "Texas Instruments", "microchip.com": "Microchip", "analog.com": "Analog Devices",
30
+ "nxp.com": "NXP", "onsemi.com": "onsemi", "vishay.com": "Vishay", "infineon.com": "Infineon", "diodes.com": "Diodes Inc.",
31
+ "nexperia.com": "Nexperia", "maximintegrated.com": "Maxim Integrated", "allegromicro.com": "Allegro MicroSystems",
32
+ "littelfuse.com": "Littelfuse", "power.com": "Power Integrations", "powerint.com": "Power Integrations",
33
+ "silabs.com": "Silicon Labs", "murata.com": "Murata", "wolfspeed.com": "Wolfspeed", "skyworksinc.com": "Skyworks",
34
+ "semicon-storage.com": "Toshiba", "toshiba.com": "Toshiba", "rohm.com": "ROHM", "broadcom.com": "Broadcom",
35
+ "kemet.com": "KEMET", "diotec.com": "Diotec", "cypress.com": "Cypress", "renesas.com": "Renesas", "akm.com": "AKM",
36
+ "bourns.com": "Bourns", "issi.com": "ISSI", "wch-ic.com": "WCH", "microsemi.com": "Microsemi", "osram-os.com": "ams OSRAM",
37
+ "espressif.com": "Espressif", "irf.com": "Infineon (IR)", "latticesemi.com": "Lattice", "ftdichip.com": "FTDI",
38
+ "ams.com": "ams OSRAM", "sitime.com": "SiTime", "xilinx.com": "AMD Xilinx", "altera.com": "Intel (Altera)",
39
+ "intersil.com": "Renesas (Intersil)", "nordicsemi.com": "Nordic Semiconductor", "linear.com": "Analog Devices",
40
+ "fairchildsemi.com": "onsemi (Fairchild)", "ixys.com": "IXYS", "mps.com": "Monolithic Power Systems",
41
+ "monolithicpower.com": "Monolithic Power Systems", "richtek.com": "Richtek", "semtech.com": "Semtech",
42
+ "torexsemi.com": "Torex", "ablic.com": "ABLIC", "sii-ic.com": "ABLIC", "jst-mfg.com": "JST", "molex.com": "Molex",
43
+ "hirose.com": "Hirose", "we-online.com": "Wurth Elektronik", "te.com": "TE Connectivity", "kingbright.com": "Kingbright",
44
+ "kingbrightusa.com": "Kingbright", "liteon.com": "Lite-On", "panasonic.com": "Panasonic", "lcsc.com": None,
45
+ "krhro.com": "HRO", "samtec.com": "Samtec", "amphenol.com": "Amphenol", "cui.com": "CUI", "sameskydevices.com": "Same Sky",
46
+ "melexis.com": "Melexis", "zilog.com": "Zilog", "atmel.com": "Microchip (Atmel)",
47
+ "national.com": "Texas Instruments (National)", "awinic.com": "Awinic", "gigadevice.com": "GigaDevice",
48
+ "winbond.com": "Winbond", "neutrik.com": "Neutrik", "cuidevices.com": "CUI Devices",
49
+ "switchcraft.com": "Switchcraft", "lumberg.com": "Lumberg", "amphenolrf.com": "Amphenol RF", "kycon.com": "Kycon", "macronix.com": "Macronix", "cree.com": "Wolfspeed (Cree)", "ween-semi.com": "WeEn"}
50
+ MPN_RE = re.compile(r"[A-Z0-9][A-Z0-9.+/-]{3,}")
51
+ WILDCARD = re.compile(r"(?<![0-9])x|x(?![0-9])")
52
+ CHIP_RE = re.compile(r"(R|C|L|LED)_(\d{4})_(\d{4})Metric")
53
+ CONNECTOR_MPN = [ # footprint-name pattern -> (manufacturer, regex group with the MPN)
54
+ (re.compile(r"JST_[A-Z]+_([A-Z0-9]+-[A-Z0-9-]+?)_\d+x\d+"), "JST"),
55
+ (re.compile(r"Molex_[A-Za-z0-9.-]+?_(\d{5,6}-\d{3,4})_\d+x\d+"), "Molex"),
56
+ (re.compile(r"Hirose_[A-Za-z0-9.-]+?_(DF\d+[A-Z0-9-]+?)_\d+x\d+"), "Hirose"),
57
+ (re.compile(r"USB_C_Receptacle_HRO_(TYPE-C-\d+-[A-Z0-9-]+)$"), "HRO"),
58
+ (re.compile(r"USB_C_Receptacle_GCT_(USB\d{4}[A-Z0-9-]*)"), "GCT"),
59
+ (re.compile(r"Wuerth_[A-Za-z0-9-]+?_(\d{9,12})_"), "Wurth Elektronik"),
60
+ (re.compile(r"Samtec_([A-Z]{3,4}-1\d\d-\d\d-[A-Z0-9-]+?)_\d+x\d+"), "Samtec"),
61
+ ]
62
+ CONNECTOR_LIBS = ("Connector_JST", "Connector_Molex", "Connector_Hirose", "Connector_USB", "Connector_Wuerth", "Connector_Samtec")
63
+
64
+ # chip passives by ordering code; (MPN, manufacturer, KiCad footprint, series datasheet or "")
65
+ _Y = "https://www.yageo.com/upload/media/product/productsearch/datasheet/rchip/PYu-RC_Group_51_RoHS_L_12.pdf"
66
+ _P = "https://industrial.panasonic.com/cdbs/www-data/pdf/RDA0000/AOA0000C304.pdf"
67
+ _K = "https://content.kemet.com/datasheets/KEM_C1002_X7R_SMD.pdf"
68
+ CURATED = [
69
+ ("RC0201FR-0710KL", "Yageo", "Resistor_SMD:R_0201_0603Metric", _Y), ("RC0402FR-0710KL", "Yageo", "Resistor_SMD:R_0402_1005Metric", _Y),
70
+ ("RC0603FR-0710KL", "Yageo", "Resistor_SMD:R_0603_1608Metric", _Y), ("RC0805FR-0710KL", "Yageo", "Resistor_SMD:R_0805_2012Metric", _Y),
71
+ ("RC1206FR-0710KL", "Yageo", "Resistor_SMD:R_1206_3216Metric", _Y), ("RC1210FR-0710KL", "Yageo", "Resistor_SMD:R_1210_3225Metric", _Y),
72
+ ("RC2010FK-0710KL", "Yageo", "Resistor_SMD:R_2010_5025Metric", _Y), ("RC2512FK-0710RL", "Yageo", "Resistor_SMD:R_2512_6332Metric", _Y),
73
+ ("ERJ-1GNF1002C", "Panasonic", "Resistor_SMD:R_0201_0603Metric", _P), ("ERJ-2RKF1002X", "Panasonic", "Resistor_SMD:R_0402_1005Metric", _P),
74
+ ("ERJ-3EKF1002V", "Panasonic", "Resistor_SMD:R_0603_1608Metric", _P), ("ERJ-6ENF1002V", "Panasonic", "Resistor_SMD:R_0805_2012Metric", _P),
75
+ ("ERJ-8ENF1002V", "Panasonic", "Resistor_SMD:R_1206_3216Metric", _P), ("ERJ-14NF1002U", "Panasonic", "Resistor_SMD:R_1210_3225Metric", _P),
76
+ ("GRM033R61A104KE15D", "Murata", "Capacitor_SMD:C_0201_0603Metric", ""), ("GRM155R71C104KA88D", "Murata", "Capacitor_SMD:C_0402_1005Metric", ""),
77
+ ("GRM188R71H104KA93D", "Murata", "Capacitor_SMD:C_0603_1608Metric", ""), ("GRM21BR71H104KA01L", "Murata", "Capacitor_SMD:C_0805_2012Metric", ""),
78
+ ("GRM31CR71H475KA12L", "Murata", "Capacitor_SMD:C_1206_3216Metric", ""), ("GRM32ER71H106KA12L", "Murata", "Capacitor_SMD:C_1210_3225Metric", ""),
79
+ ("CL03A104KP3NNNC", "Samsung", "Capacitor_SMD:C_0201_0603Metric", ""), ("CL05B104KO5NNNC", "Samsung", "Capacitor_SMD:C_0402_1005Metric", ""),
80
+ ("CL10B104KB8NNNC", "Samsung", "Capacitor_SMD:C_0603_1608Metric", ""), ("CL21B104KBCNNNC", "Samsung", "Capacitor_SMD:C_0805_2012Metric", ""),
81
+ ("CL31A106KBHNNNE", "Samsung", "Capacitor_SMD:C_1206_3216Metric", ""), ("C0402C104K4RACTU", "KEMET", "Capacitor_SMD:C_0402_1005Metric", _K),
82
+ ("C0603C104K5RACTU", "KEMET", "Capacitor_SMD:C_0603_1608Metric", _K), ("C0805C104K5RACTU", "KEMET", "Capacitor_SMD:C_0805_2012Metric", _K),
83
+ ("C1206C104K5RACTU", "KEMET", "Capacitor_SMD:C_1206_3216Metric", _K), ("C1812C104K5RACTU", "KEMET", "Capacitor_SMD:C_1812_4532Metric", _K),
84
+ ("LQG15HS10NJ02D", "Murata", "Inductor_SMD:L_0402_1005Metric", ""), ("LQG18HN10NJ00D", "Murata", "Inductor_SMD:L_0603_1608Metric", ""),
85
+ ("LTST-C191KRKT", "Lite-On", "LED_SMD:LED_0603_1608Metric", ""), ("LTST-C171KRKT", "Lite-On", "LED_SMD:LED_0805_2012Metric", ""),
86
+ ("LTST-C150KRKT", "Lite-On", "LED_SMD:LED_1206_3216Metric", ""), ("ERJ-2GE0R00X", "Panasonic", "Resistor_SMD:R_0402_1005Metric", _P),
87
+ ]
88
+
89
+
90
+ def classify(fp):
91
+ """'Lib:Name' -> family or None."""
92
+ lib, name = fp.split(":", 1)
93
+ n = name.upper()
94
+ if CHIP_RE.fullmatch(name.split("_Pad")[0].split("_Hand")[0]) or CHIP_RE.match(name):
95
+ return "chip"
96
+ if lib.startswith("Connector") or lib in ("Connector_Audio", "Connector_RJ"):
97
+ return "connector"
98
+ if lib == "Package_BGA" or lib == "Package_CSP" or n.startswith(("BGA", "WLCSP", "LFBGA", "UFBGA", "TFBGA")):
99
+ return "bga"
100
+ if lib in ("Package_DFN_QFN", "Package_SON") or re.match(r"(V|W|U|X|T)?(QFN|DFN|SON)", n):
101
+ return "qfn"
102
+ if lib == "Package_QFP":
103
+ return "qfp"
104
+ if lib == "Package_SO":
105
+ if n.startswith(("SOIC", "SO-", "SOP-")) and "SSOP" not in n:
106
+ return "soic"
107
+ if re.match(r"(H?TSSOP|MSOP|SSOP|QSOP|VSSOP|TSOP|HVSSOP|VSOP|TVSOP)", n):
108
+ return "tssop"
109
+ return None
110
+ if lib == "Package_TO_SOT_SMD":
111
+ if re.match(r"(SOT|SC-|TSOT|SOT-?\d)", n):
112
+ return "sot"
113
+ if re.match(r"(TO-252|TO-263|TO-268|D2?PAK|DPAK|TO-277|POWERDI|PQFN|LFPAK|TDSON|PowerPAK)", n, re.I):
114
+ return "power"
115
+ return None
116
+ if lib == "Diode_SMD" and re.match(r"D_(SOD|SMA|SMB|SMC|MELF|MINIMELF|MICROMELF|SOD-?\d|PowerDI)", n):
117
+ return "diode"
118
+ if lib in ("Package_DIP", "Package_TO_SOT_THT", "Diode_THT"):
119
+ return "tht"
120
+ return None
121
+
122
+
123
+ def short_package(fp):
124
+ """What a BOM's Package column would say: the footprint name up to its first '_', maker prefixes skipped
125
+ (D_SOD-123 -> SOD-123, R_0603_1608Metric -> 0603, Maxim_TDFN-12-1EP_... -> TDFN-12-1EP). Connectors: none (a
126
+ BOM names them by MPN only)."""
127
+ lib, name = fp.split(":", 1)
128
+ m = CHIP_RE.match(name)
129
+ if m:
130
+ return m.group(2)
131
+ if lib.startswith("Connector"):
132
+ return ""
133
+ toks = re.sub(r"^(D|LED)_", "", name).split("_")
134
+ return next((t for t in toks if re.search(r"\d", t)), toks[-1])
135
+
136
+
137
+ def maker(url):
138
+ m = re.match(r"https?://([^/:]+)", url or "")
139
+ if not m:
140
+ return None
141
+ parts = m.group(1).lower().split(".")
142
+ for k in range(len(parts) - 1):
143
+ d = ".".join(parts[k:])
144
+ if d in MAKERS:
145
+ return MAKERS[d]
146
+ return None
147
+
148
+
149
+ def pins_of(chain):
150
+ """[(num, name)] of a KiCad symbol (extends chain), one per pin number, names as drawn ('~' = none)."""
151
+ out = {}
152
+ for s in reversed(chain):
153
+ for sub in sexpr.find(s, "symbol"):
154
+ for p in sexpr.find(sub, "pin"):
155
+ num = (sexpr.first(p, "number") or ["", ""])[1]
156
+ name = (sexpr.first(p, "name") or ["", ""])[1]
157
+ out.setdefault(str(num), str(name))
158
+ return sorted(out.items(), key=lambda kv: _natural(kv[0]))
159
+
160
+
161
+ def _natural(s):
162
+ return [int(t) if t.isdigit() else t for t in re.split(r"(\d+)", str(s))]
163
+
164
+
165
+ def _git_head(d):
166
+ try:
167
+ return subprocess.run(["git", "-C", str(d), "log", "-1", "--format=%H %cs"], capture_output=True, text=True,
168
+ check=True).stdout.strip()
169
+ except (OSError, subprocess.CalledProcessError):
170
+ return "unknown"
171
+
172
+
173
+ def symbol_candidates(sym_dir):
174
+ for d in sorted(Path(sym_dir).glob("*.kicad_symdir")):
175
+ for f in sorted(d.glob("*.kicad_sym")):
176
+ for s in sexpr.find(sexpr.parse(f.read_text(encoding="utf-8")), "symbol"):
177
+ name = s[1]
178
+ if not MPN_RE.fullmatch(name) or not re.search(r"\d", name) or WILDCARD.search(name):
179
+ continue
180
+ try:
181
+ chain, _ = K.load_symbol(f"{f}:{name}", None)
182
+ except (FileNotFoundError, ValueError):
183
+ continue
184
+ props = K._props(chain)
185
+ fp, ds = props.get("Footprint", ""), props.get("Datasheet", "")
186
+ if ":" not in fp or not ds.startswith("http"):
187
+ continue
188
+ yield {"mpn": name, "symbol": f"{d.name[:-len('.kicad_symdir')]}:{name}", "footprint": fp, "datasheet": ds,
189
+ "description": props.get("Description", ""), "pins": pins_of(chain)}
190
+
191
+
192
+ def connector_candidates(fp_dir):
193
+ for lib in CONNECTOR_LIBS:
194
+ for f in sorted((Path(fp_dir) / f"{lib}.pretty").glob("*.kicad_mod")):
195
+ name = f.stem
196
+ for rx, mk in CONNECTOR_MPN:
197
+ m = rx.search(name)
198
+ if m:
199
+ text = f.read_text(encoding="utf-8")
200
+ descr = (sexpr.first(sexpr.parse(text), "descr") or ["", ""])[1]
201
+ url = re.search(r"https?://[^\s,)\"]+", descr)
202
+ nums = sorted({p[0] for p in FB.numbers(text)["pads"] if re.fullmatch(r"\d+", p[0] or "")}, key=int)
203
+ yield {"mpn": m.group(1), "manufacturer": mk, "footprint": f"{lib}:{name}",
204
+ "datasheet": url.group(0) if url else "", "description": descr[:200],
205
+ "pins": [(n, "") for n in nums]}
206
+ break
207
+
208
+
209
+ def spread(items, n):
210
+ """n items evenly spread over a sorted list (deterministic)."""
211
+ if len(items) <= n:
212
+ return list(items)
213
+ step = len(items) / n
214
+ return [items[int(i * step)] for i in range(n)]
215
+
216
+
217
+ def build(sym_dir, fp_dir, quota=None, per_footprint=PER_FOOTPRINT, log=print):
218
+ quota = dict(QUOTA, **(quota or {}))
219
+ refs, cases, fam_pool = {}, [], {}
220
+
221
+ def ref(fp):
222
+ if fp not in refs:
223
+ lib, name = fp.split(":", 1)
224
+ f = Path(fp_dir) / f"{lib}.pretty" / f"{name}.kicad_mod"
225
+ refs[fp] = FB.numbers(f.read_text(encoding="utf-8")) if f.exists() else None
226
+ return refs[fp]
227
+
228
+ for c in symbol_candidates(sym_dir):
229
+ fam = classify(c["footprint"])
230
+ if fam and fam != "chip":
231
+ c.update(manufacturer=maker(c["datasheet"]) or "", **{"from": {"kind": "kicad-symbol", "symbol": c["symbol"]}})
232
+ fam_pool.setdefault(fam, []).append(c)
233
+ for c in connector_candidates(fp_dir):
234
+ c["from"] = {"kind": "kicad-footprint", "footprint": c["footprint"]}
235
+ fam_pool.setdefault("connector", []).append(c)
236
+ for mpn, mk, fp, ds in CURATED:
237
+ kind = CHIP_RE.match(fp.split(":")[1]).group(1)
238
+ fam_pool.setdefault("chip", []).append({"mpn": mpn, "manufacturer": mk, "footprint": fp, "datasheet": ds,
239
+ "description": {"R": "chip resistor", "C": "chip capacitor", "L": "chip inductor",
240
+ "LED": "chip LED"}[kind],
241
+ "pins": [("1", "~"), ("2", "~")] if kind != "LED" else [("1", "K"), ("2", "A")],
242
+ "from": {"kind": "curated", "note": "size code in the ordering code fixes the chip footprint"
243
+ + ("; series datasheet link not fetched here" if ds else "")}})
244
+ for fam, n in quota.items():
245
+ pool, seen_mpn = [], set()
246
+ per = {}
247
+ for c in sorted(fam_pool.get(fam, []), key=lambda c: (c["footprint"], c["mpn"])):
248
+ if c["mpn"] in seen_mpn or (fam != "chip" and per.get(c["footprint"], 0) >= per_footprint) or not ref(c["footprint"]) \
249
+ or not ref(c["footprint"])["pads"]:
250
+ continue
251
+ if fam != "connector" and not c["manufacturer"] and fam != "chip":
252
+ continue # a distributor / archive link: no maker to put in the BOM
253
+ per[c["footprint"]] = per.get(c["footprint"], 0) + 1
254
+ seen_mpn.add(c["mpn"])
255
+ pool.append(c)
256
+ chosen = spread(pool, n)
257
+ log(f" {fam:<10} {len(chosen):>3} of {len(pool)} candidates")
258
+ for c in chosen:
259
+ cases.append({"id": f"{fam}-{re.sub(r'[^A-Za-z0-9.-]', '_', c['mpn'])}", "family": fam, "mpn": c["mpn"],
260
+ "manufacturer": c["manufacturer"], "package": short_package(c["footprint"]),
261
+ "footprint": c["footprint"], "datasheet": c["datasheet"], "description": c["description"][:200],
262
+ "pins": [list(p) for p in c["pins"]], "from": c["from"]})
263
+ used = {c["footprint"] for c in cases}
264
+ return cases, {k: v for k, v in refs.items() if k in used}
265
+
266
+
267
+ def write_set(path, cases, refs, sym_dir, fp_dir, name):
268
+ meta = {"name": name, "version": 1, "built": datetime.date.today().isoformat(),
269
+ "kicad_symbols": _git_head(sym_dir), "kicad_footprints": _git_head(fp_dir),
270
+ "license": "Numbers only (pad centres, sizes, drills, courtyard box; pin numbers and names) from KiCad's official "
271
+ "libraries, https://gitlab.com/kicad/libraries, CC-BY-SA 4.0 with the KiCad libraries exception.",
272
+ "built_with": "ecadlib eval build-set (ecadlib/eval/build_set.py)", "cases": len(cases)}
273
+ lines = ["{", f'"meta": {json.dumps(meta)},', '"cases": [']
274
+ lines += [json.dumps(c, separators=(",", ":")) + ("," if i < len(cases) - 1 else "") for i, c in enumerate(cases)]
275
+ lines += ["],", '"references": {']
276
+ items = sorted(refs.items())
277
+ lines += [f"{json.dumps(k)}: {json.dumps(v, separators=(',', ':'))}" + ("," if i < len(items) - 1 else "")
278
+ for i, (k, v) in enumerate(items)]
279
+ lines += ["}", "}"]
280
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
281
+ Path(path).write_text("\n".join(lines) + "\n", encoding="utf-8")
282
+
283
+
284
+ def main(argv=None):
285
+ ap = argparse.ArgumentParser(prog="ecadlib eval build-set", description=__doc__.split("\n")[0])
286
+ ap.add_argument("--symbols", required=True, help="kicad-symbols checkout (git clone --depth 1 https://gitlab.com/kicad/libraries/kicad-symbols)")
287
+ ap.add_argument("--footprints", required=True, help="kicad-footprints checkout")
288
+ ap.add_argument("-o", "--out", help="set file (default: ecadlib/eval/sets/<name>.json)")
289
+ ap.add_argument("--name", default="kicad-v1")
290
+ a = ap.parse_args(argv)
291
+ cases, refs = build(a.symbols, a.footprints)
292
+ out = a.out or Path(__file__).with_name("sets") / f"{a.name}.json"
293
+ write_set(out, cases, refs, a.symbols, a.footprints, a.name)
294
+ print(f"{len(cases)} cases, {len(refs)} reference footprints -> {out}")
295
+ return 0
296
+
297
+
298
+ if __name__ == "__main__":
299
+ sys.exit(main())
ecadlib/eval/cli.py ADDED
@@ -0,0 +1,119 @@
1
+ """ecadlib eval: measure how often the pipeline gets a part right, and what it costs.
2
+
3
+ ecadlib eval run [--set kicad-v1] [--limit N] [--family F,..] [--case ID,..] [--model M] [--sources S,..]
4
+ [--record DIR | --replay DIR] [--repeat N] [--offline] [-o out/eval/run]
5
+ ecadlib eval consistency A B two runs of the same cases: parts that gave the same files (and cross-check)
6
+ ecadlib eval compare A B two run folders (or report.json files): pass rate and cost per family, flipped cases
7
+ ecadlib eval sets the test sets shipped with ecadlib (cases per family)
8
+ ecadlib eval show [--set NAME] [--family F] the cases of a set
9
+ ecadlib eval build-set --symbols DIR --footprints DIR [-o FILE] rebuild a set from KiCad library checkouts
10
+ ecadlib eval gate [--update] CI accuracy gate: the whole set offline vs a committed baseline (gate.py)
11
+ ecadlib eval golden [--update] golden snapshots of the KiCad / Altium files we write (golden.py)
12
+
13
+ See ecadlib/eval/runner.py (run, modes), score.py (fields, tolerances) and build_set.py (where the cases come from)."""
14
+ import argparse, json, sys
15
+
16
+ from . import runner as R, score as S
17
+
18
+
19
+ def _list(s):
20
+ return [x.strip() for x in s.split(",") if x.strip()] if s else None
21
+
22
+
23
+ def main(argv=None):
24
+ argv = sys.argv[1:] if argv is None else list(argv)
25
+ if argv and argv[0] == "build-set":
26
+ from . import build_set
27
+ return build_set.main(argv[1:])
28
+ if argv and argv[0] in ("gate", "golden"):
29
+ from . import gate, golden
30
+ return {"gate": gate, "golden": golden}[argv[0]].main(argv[1:])
31
+ ap = argparse.ArgumentParser(prog="ecadlib eval", description=__doc__.split("\n")[0])
32
+ sub = ap.add_subparsers(dest="cmd", required=True)
33
+ r = sub.add_parser("run", help="run the pipeline on a test set and score every part")
34
+ r.add_argument("--set", default="kicad-v1", help="set name (ecadlib/eval/sets) or a set .json file")
35
+ r.add_argument("--limit", type=int, help="first N cases, families interleaved")
36
+ r.add_argument("--family", help="only these families (comma list)")
37
+ r.add_argument("--case", help="only these case ids or MPNs (comma list)")
38
+ r.add_argument("--model", help="datasheet model (default: [ai] / [datasheet] model)")
39
+ r.add_argument("--provider", help="anthropic | openai-compatible | openai | openrouter | gemini | ollama | lmstudio | vllm | "
40
+ "llamacpp (default: [ai] provider); compare models on the same set")
41
+ r.add_argument("--base-url", help="openai-compatible endpoint, e.g. http://localhost:11434/v1")
42
+ r.add_argument("--api-key-env", help="NAME of the env var with the key for --provider")
43
+ r.add_argument("--assist", choices=["auto", "off", "full"], help="guide rails (default: [ai] assist = auto, by tier)")
44
+ r.add_argument("--sources", help="librarian sources in order (default: the rules' order without kicad)")
45
+ r.add_argument("--providers", help="triage providers (default: the rules' order)")
46
+ g = r.add_mutually_exclusive_group()
47
+ g.add_argument("--record", metavar="DIR", help="save every Claude answer here (live)")
48
+ g.add_argument("--replay", metavar="DIR", help="answer from saved responses; nothing is sent")
49
+ r.add_argument("--repeat", type=int, default=1, metavar="N",
50
+ help="run the cases N times (the repeats from the recorded answers when --record/--replay) and report "
51
+ "how many parts gave the same files")
52
+ r.add_argument("--offline", action="store_true", help="HTTP cache only")
53
+ r.add_argument("--cache", default="out/http_cache")
54
+ r.add_argument("--rules", help="library rules file")
55
+ r.add_argument("--tol", action="append", default=[], metavar="FIELD=MM",
56
+ help=f"override a tolerance ({', '.join(f'{k}={v}' for k, v in S.TOL.items())})")
57
+ r.add_argument("-o", "--out", default="out/eval/run")
58
+ r.add_argument("--json", action="store_true", help="print the summary as JSON")
59
+ c = sub.add_parser("compare", help="diff two reports")
60
+ c.add_argument("a"); c.add_argument("b")
61
+ c.add_argument("--json", action="store_true")
62
+ sub.add_parser("gate", help="CI accuracy gate: the whole set offline against the committed baseline (--update rewrites it)")
63
+ sub.add_parser("golden", help="golden snapshots of the KiCad / Altium files (--update rewrites them)")
64
+ k = sub.add_parser("consistency", help="did two runs of the same cases give the same files?")
65
+ k.add_argument("a"); k.add_argument("b")
66
+ k.add_argument("--json", action="store_true")
67
+ sub.add_parser("sets", help="list the shipped test sets")
68
+ s = sub.add_parser("show", help="list the cases of a set")
69
+ s.add_argument("--set", default="kicad-v1"); s.add_argument("--family")
70
+ a = ap.parse_args(argv)
71
+ if a.cmd == "sets":
72
+ for n in R.available():
73
+ d = R.load_set(n)
74
+ fams = {}
75
+ for cs in d["cases"]:
76
+ fams[cs["family"]] = fams.get(cs["family"], 0) + 1
77
+ print(f"{n}: {len(d['cases'])} cases, built {d['meta'].get('built')} from kicad-symbols {d['meta'].get('kicad_symbols')}, "
78
+ f"kicad-footprints {d['meta'].get('kicad_footprints')}\n " + ", ".join(f"{k} {v}" for k, v in fams.items()))
79
+ return 0
80
+ if a.cmd == "show":
81
+ for cs in R.load_set(a.set)["cases"]:
82
+ if not a.family or cs["family"] in _list(a.family):
83
+ print(f"{cs['id']:<40} {cs['manufacturer'][:18]:<18} {cs['package']:<16} {cs['footprint']} [{cs['from']['kind']}]")
84
+ return 0
85
+ if a.cmd == "compare":
86
+ res = R.compare(a.a, a.b)
87
+ print(json.dumps({k: v for k, v in res.items() if k != "text"}, indent=1, default=str) if a.json else res["text"])
88
+ return 0
89
+ if a.cmd == "consistency":
90
+ res = R.consistency(a.a, a.b)
91
+ if a.json:
92
+ print(json.dumps(res, indent=1))
93
+ else:
94
+ print(f"{res['same']}/{res['cases']} parts gave the same files" + ("" if not res["differ"] else ":"))
95
+ for d in res["differ"]:
96
+ print(f" {d['id']}: differ in {', '.join(d['files']) or '-'}"
97
+ + (f"; cross-check {d['cross_check'][0] or '-'} vs {d['cross_check'][1] or '-'}"
98
+ if d["cross_check"][0] != d["cross_check"][1] else ""))
99
+ return 0 if not res["differ"] else 2
100
+ tol = {}
101
+ for t in a.tol:
102
+ k, _, v = t.partition("=")
103
+ if k not in S.TOL:
104
+ ap.error(f"--tol {t}: unknown field {k!r}")
105
+ tol[k] = float(v)
106
+ rep = R.run(a.set, a.out, a.limit, a.model, a.record, a.replay, _list(a.sources), _list(a.family), _list(a.case),
107
+ a.offline, a.cache, rules=a.rules, providers=a.providers, tol=tol, provider=a.provider,
108
+ base_url=a.base_url, assist=a.assist, api_key_env=a.api_key_env, repeat=a.repeat)
109
+ if a.json:
110
+ print(json.dumps(rep["summary"], indent=1))
111
+ else:
112
+ o = rep["summary"]["overall"]
113
+ print("\n".join(R.report_md(rep).splitlines()[2:6]))
114
+ print(f"report: {a.out}/report.md ({o['pass']}/{o['cases']} pass)")
115
+ return 0
116
+
117
+
118
+ if __name__ == "__main__":
119
+ sys.exit(main())