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,218 @@
1
+ """Benchmark generated footprints against KiCad's official library: every pad (position, size, drill) and the courtyard.
2
+
3
+ ecadlib footprint-bench # fetch the references (cached), compare, print pass rates
4
+ ecadlib footprint-bench --offline # from the HTTP cache / the numbers-only fixture only
5
+ ecadlib footprint-bench --save-fixture tests/fixtures/kicad_bench.json
6
+
7
+ BENCH lists ecadlib package specs (package_specs/ and package_specs/advanced/) and the official footprint each one is
8
+ compared with (gitlab.com/kicad/libraries/kicad-footprints, fetched once through httpcache). Pads are matched by
9
+ number (pads only on B.Cu, the heat-spreader copy of an exposed pad under thermal vias, are left out: the
10
+ generator does not draw them); pads that share a number (MP, SH, NPTH holes "") are matched to the nearest one of that number. A pad passes
11
+ when its centre and its size (after rotation) are within POS_TOL / SIZE_TOL and a through-hole pad's drill matches;
12
+ the courtyard passes when every edge of its bounding box is within CRT_TOL. Reported: pads passing, footprints whose
13
+ pads all pass, courtyards passing, and footprint_check findings on our footprint.
14
+
15
+ The fixture keeps only numbers (pad centres, sizes, drills, courtyard box) from the KiCad files (CC-BY-SA 4.0; facts,
16
+ not the drawings), so the tests run offline.
17
+ """
18
+ import argparse, json, math, sys
19
+ from pathlib import Path
20
+
21
+ from . import footprint_check as C, footprint_gen as F, sexpr
22
+
23
+ KICAD_RAW = "https://gitlab.com/kicad/libraries/kicad-footprints/-/raw/master/"
24
+ SPECS = Path(__file__).with_name("package_specs")
25
+ POS_TOL, SIZE_TOL, CRT_TOL = 0.01, 0.01, 0.011
26
+ # (spec name, KiCad library, what the comparison shows)
27
+ BENCH = [
28
+ ("BGA-256_17.0x17.0mm_Layout16x16_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD", "Package_BGA", "IPC land from the ball size"),
29
+ ("BGA-100_11.0x11.0mm_Layout10x10_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD", "Package_BGA", "IPC land from the ball size"),
30
+ ("BGA-324_15.0x15.0mm_Layout18x18_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD", "Package_BGA", "18 rows: A..V without I O Q S"),
31
+ ("BGA-144_7.0x7.0mm_Layout13x13_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD", "Package_BGA", "depopulated centre E5:J9"),
32
+ ("UFBGA-132_7x7mm_Layout12x12_P0.5mm", "Package_BGA", "ball map with a missing ring; datasheet land"),
33
+ ("PinHeader_1x05_P2.54mm_Vertical", "Connector_PinHeader_2.54mm", "THT header table"),
34
+ ("PinHeader_2x05_P2.54mm_Vertical", "Connector_PinHeader_2.54mm", "dual-row zig-zag numbering"),
35
+ ("PinHeader_1x05_P2.00mm_Vertical", "Connector_PinHeader_2.00mm", "THT header table"),
36
+ ("PinHeader_2x05_P1.27mm_Vertical", "Connector_PinHeader_1.27mm", "body offset from the pins"),
37
+ ("PinHeader_2x05_P1.27mm_Vertical_SMD", "Connector_PinHeader_1.27mm", "SMD header land"),
38
+ ("PinHeader_2x05_P2.54mm_Vertical_SMD", "Connector_PinHeader_2.54mm", "SMD header land"),
39
+ ("PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Left", "Connector_PinHeader_2.54mm", "single row, alternating sides"),
40
+ ("JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical", "Connector_JST", "THT pads sized from the drill"),
41
+ ("JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical", "Connector_JST", "THT pads sized from the drill"),
42
+ ("JST_PH_S4B-PH-SM4-TB_1x04-1MP_P2.00mm_Horizontal", "Connector_JST", "SMD + mounting pads, y-up drawing"),
43
+ ("JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal", "Connector_JST", "SMD + mounting pads"),
44
+ ("Molex_KK-254_AE-6410-04A_1x04_P2.54mm_Vertical", "Connector_Molex", "table in mil"),
45
+ ("Molex_PicoBlade_53398-0471_1x04-1MP_P1.25mm_Vertical", "Connector_Molex", "bottom-view drawing from pin 1"),
46
+ ("USB_C_Receptacle_HRO_TYPE-C-31-M-12", "Connector_USB", "stacked pads, slots, NPTH, datum origin"),
47
+ ("SOT-23-6", "Package_TO_SOT_SMD", "gull-wing, uneven rows"),
48
+ # more ball grids: double row letters, other depopulation patterns, datasheet lands, chip-scale
49
+ ("BGA-121_9.0x9.0mm_Layout11x11_P0.8mm_Ball0.4mm_Pad0.35mm_NSMD", "Package_BGA", "datasheet land (IPC gives 0.30)"),
50
+ ("BGA-169_11.0x11.0mm_Layout13x13_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD", "Package_BGA", "13 rows A..N without I"),
51
+ ("BGA-256_11.0x11.0mm_Layout20x20_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD", "Package_BGA", "20 rows A..Y, centre E5:T16 empty"),
52
+ ("BGA-672_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD", "Package_BGA", "rows AA..AF, corner balls (A1) missing"),
53
+ ("BGA-36_3.396x3.466mm_Layout6x6_P0.4mm_Ball0.25mm_Pad0.2mm_NSMD", "Package_BGA", "WLCSP, 0.4 mm pitch, oblong body"),
54
+ ("BGA-68_5.0x5.0mm_Layout9x9_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD", "Package_BGA", "13 single balls / blocks missing"),
55
+ ("BGA-90_8.0x13.0mm_Layout2x3x15_P0.8mm", "Package_BGA", "2 x 3 x 15: empty middle columns"),
56
+ ("LFBGA-144_10x10mm_Layout12x12_P0.8mm", "Package_BGA", "12 x 12 full grid"),
57
+ # thermal vias in an exposed pad (custom pad table)
58
+ ("QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm_ThermalVias", "Package_DFN_QFN", "EP with 2 x 2 vias, split paste"),
59
+ ("DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias", "Package_DFN_QFN", "EP with 2 x 3 vias, two pitches"),
60
+ # connectors and headers
61
+ ("JST_PH_B4B-PH-K_1x04_P2.00mm_Vertical", "Connector_JST", "THT pads sized from the drill"),
62
+ ("JST_XH_B6B-XH-A_1x06_P2.50mm_Vertical", "Connector_JST", "y-down drawing from pin 1"),
63
+ ("JST_GH_SM04B-GHS-TB_1x04-1MP_P1.25mm_Horizontal", "Connector_JST", "SMD + mounting pads"),
64
+ ("PinHeader_2x20_P2.54mm_Vertical", "Connector_PinHeader_2.54mm", "40 pins zig-zag"),
65
+ ("PinHeader_1x10_P1.00mm_Vertical", "Connector_PinHeader_1.00mm", "1.00 mm THT table"),
66
+ ("PinHeader_1x05_P2.00mm_Vertical_SMD_Pin1Left", "Connector_PinHeader_2.00mm", "single-row SMD land table"),
67
+ ("PinHeader_1x05_P1.27mm_Vertical_SMD_Pin1Left", "Connector_PinHeader_1.27mm", "single-row SMD land table"),
68
+ ("PinHeader_1x05_P1.00mm_Vertical_SMD_Pin1Left", "Connector_PinHeader_1.00mm", "single-row SMD land table"),
69
+ ("PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Right", "Connector_PinHeader_2.54mm", "single row, pin 1 on the right"),
70
+ ("PinHeader_2x05_P2.00mm_Vertical_SMD", "Connector_PinHeader_2.00mm", "SMD header land table"),
71
+ ("PinHeader_2x05_P1.00mm_Vertical_SMD", "Connector_PinHeader_1.00mm", "SMD header land table"),
72
+ # IPC-7351B parts KiCad generates from the same body dimensions
73
+ ("R_0402_1005Metric", "Resistor_SMD", "chip"), ("R_0603_1608Metric", "Resistor_SMD", "chip"),
74
+ ("R_0805_2012Metric", "Resistor_SMD", "chip"), ("R_1206_3216Metric", "Resistor_SMD", "chip"),
75
+ ("C_0805_2012Metric", "Capacitor_SMD", "chip"), ("LED_0603_1608Metric", "LED_SMD", "chip"),
76
+ ("LED_0805_2012Metric", "LED_SMD", "chip"), ("SOT-23", "Package_TO_SOT_SMD", "gull-wing"),
77
+ ("SOT-23-5", "Package_TO_SOT_SMD", "gull-wing, uneven rows"), ("SOIC-8_3.9x4.9mm_P1.27mm", "Package_SO", "gull-wing"),
78
+ ("SOIC-16_3.9x9.9mm_P1.27mm", "Package_SO", "gull-wing"), ("TSSOP-16_4.4x5mm_P0.65mm", "Package_SO", "gull-wing, fine pitch"),
79
+ ("LQFP-48_7x7mm_P0.5mm", "Package_QFP", "quad gull-wing"),
80
+ ("QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm", "Package_DFN_QFN", "no-lead + EP"),
81
+ ("QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm", "Package_DFN_QFN", "no-lead + EP"),
82
+ ("VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm", "Package_DFN_QFN", "no-lead + EP"),
83
+ ]
84
+
85
+
86
+ def spec_path(name):
87
+ for d in (SPECS, SPECS / "advanced"):
88
+ if (d / f"{name}.json").exists():
89
+ return d / f"{name}.json"
90
+ raise FileNotFoundError(f"no package spec {name}")
91
+
92
+
93
+ def numbers(text):
94
+ """The numbers the comparison needs from a .kicad_mod: pads (num, x, y, w, h, drill or 0) and the courtyard box."""
95
+ t = sexpr.parse(text)
96
+ pads = []
97
+ for p, r in zip(sexpr.find(t, "pad"), C._pad_rects(t)):
98
+ if not any(l.startswith(("F.", "*.", "F&B.")) for l in r["layers"]):
99
+ continue # B.Cu-only copy of an exposed pad (heat spreader): not compared
100
+ d = sexpr.first(p, "drill")
101
+ drill = max((float(v) for v in d[1:] if isinstance(v, str) and v != "oval"), default=0.0) if d else 0.0 # not (offset x y);
102
+ # an SMD pad's (drill (offset ...)) alone is no hole
103
+ pads.append([r["num"], round(r["x"], 4), round(r["y"], 4), round(r["w"], 4), round(r["h"], 4), drill])
104
+ pads = [p for p in pads if p[0] or p[5]] # paste-only windows are not pads
105
+ return {"pads": pads, "courtyard": list(sexpr.courtyard_bbox(t) or ())}
106
+
107
+
108
+ def compare(mine, ref):
109
+ """mine/ref: numbers() dicts -> report dict."""
110
+ left = list(ref["pads"])
111
+ rows, missing = [], []
112
+ for p in mine["pads"]:
113
+ cands = [q for q in left if q[0] == p[0]]
114
+ if not cands:
115
+ missing.append(p[0] or "(hole)")
116
+ continue
117
+ q = min(cands, key=lambda q: math.hypot(q[1] - p[1], q[2] - p[2]))
118
+ left.remove(q)
119
+ dpos = max(abs(p[1] - q[1]), abs(p[2] - q[2]))
120
+ dsize = max(abs(p[3] - q[3]), abs(p[4] - q[4]))
121
+ ddrill = abs(p[5] - q[5])
122
+ rows.append({"pad": p[0], "dpos": round(dpos, 4), "dsize": round(dsize, 4), "ddrill": round(ddrill, 4),
123
+ "ok": dpos <= POS_TOL + 1e-9 and dsize <= SIZE_TOL + 1e-9 and ddrill <= SIZE_TOL + 1e-9})
124
+ a, b = mine["courtyard"], ref["courtyard"]
125
+ dcrt = max(abs(x - y) for x, y in zip(a, b)) if a and b else None
126
+ ok_pads = sum(r["ok"] for r in rows)
127
+ total = max(len(mine["pads"]), len(ref["pads"]))
128
+ return {"pads": total, "pads_ok": ok_pads, "extra_in_ours": missing, "missing_in_ours": [q[0] or "(hole)" for q in left],
129
+ "worst_pos": max((r["dpos"] for r in rows), default=None), "worst_size": max((r["dsize"] for r in rows), default=None),
130
+ "courtyard_ours": a, "courtyard_kicad": b, "courtyard_diff": None if dcrt is None else round(dcrt, 4),
131
+ "courtyard_ok": dcrt is not None and dcrt <= CRT_TOL, "all_pads_ok": ok_pads == total, "rows": rows}
132
+
133
+
134
+ def reference(name, lib, http=None, fixture=None):
135
+ """KiCad's numbers for one footprint: from the fixture, else fetched (and cached) from GitLab."""
136
+ if fixture and name in fixture:
137
+ return fixture[name]
138
+ if http is None:
139
+ return None
140
+ r = http.get(f"{KICAD_RAW}{lib}.pretty/{name}.kicad_mod")
141
+ return numbers(r.text) if r.ok else None
142
+
143
+
144
+ def run(bench=BENCH, http=None, fixture=None):
145
+ results = []
146
+ for name, lib, what in bench:
147
+ spec = json.loads(spec_path(name).read_text())
148
+ F.validate(spec)
149
+ txt, rep = F.build(spec)
150
+ ref = reference(name, lib, http, fixture)
151
+ res = {"name": name, "library": lib, "shows": what, "type": spec["package_type"],
152
+ "check_errors": [c["message"] for c in rep["checks"] if c["level"] == "error"]}
153
+ if ref is None:
154
+ res["error"] = "reference not available (offline and not in the fixture)"
155
+ else:
156
+ res.update(compare(numbers(txt), ref))
157
+ results.append(res)
158
+ done = [r for r in results if "error" not in r]
159
+ summary = {"footprints": len(done), "unavailable": len(results) - len(done),
160
+ "pads": sum(r["pads"] for r in done), "pads_ok": sum(r["pads_ok"] for r in done),
161
+ "footprints_all_pads_ok": sum(r["all_pads_ok"] for r in done), "courtyards_ok": sum(r["courtyard_ok"] for r in done),
162
+ "check_clean": sum(not r["check_errors"] for r in results)}
163
+ return results, summary
164
+
165
+
166
+ def report(results, summary):
167
+ out = [f"{'footprint':<58} {'pads ok':>9} {'worst pos/size':>15} {'courtyard':>10} notes"]
168
+ for r in results:
169
+ if "error" in r:
170
+ out.append(f"{r['name']:<58} {'-':>9} {'':>15} {'':>10} {r['error']}")
171
+ continue
172
+ crt = "ok" if r["courtyard_ok"] else f"{r['courtyard_diff']:+.2f}" if r["courtyard_diff"] is not None else "-"
173
+ note = r["shows"]
174
+ if r["missing_in_ours"] or r["extra_in_ours"]:
175
+ note += f"; KiCad-only pads {r['missing_in_ours']}, ours-only {r['extra_in_ours']}"
176
+ if not r["courtyard_ok"] and r["courtyard_kicad"]:
177
+ note += f"; courtyard ours {r['courtyard_ours']} KiCad {r['courtyard_kicad']}"
178
+ out.append(f"{r['name']:<58} {r['pads_ok']:>4}/{r['pads']:<4} {r['worst_pos']:>7.3f}/{r['worst_size']:<7.3f} {crt:>10} {note}")
179
+ s = summary
180
+ pct = lambda a, b: f"{100 * a / b:.0f} %" if b else "-"
181
+ out += ["", f"pads within {POS_TOL} mm / {SIZE_TOL} mm: {s['pads_ok']}/{s['pads']} ({pct(s['pads_ok'], s['pads'])}); "
182
+ f"footprints with every pad matching: {s['footprints_all_pads_ok']}/{s['footprints']} "
183
+ f"({pct(s['footprints_all_pads_ok'], s['footprints'])}); courtyards within {CRT_TOL:.2f} mm: "
184
+ f"{s['courtyards_ok']}/{s['footprints']} ({pct(s['courtyards_ok'], s['footprints'])}); "
185
+ f"footprint_check clean: {s['check_clean']}/{s['footprints'] + s['unavailable']}"]
186
+ return "\n".join(out)
187
+
188
+
189
+ def main(argv=None):
190
+ ap = argparse.ArgumentParser(prog="ecadlib footprint-bench", description=__doc__.split("\n")[0])
191
+ ap.add_argument("--offline", action="store_true", help="use only the HTTP cache and the fixture")
192
+ ap.add_argument("--fixture", help="numbers-only reference file to read (default: none; fetch from GitLab)")
193
+ ap.add_argument("--save-fixture", help="write the KiCad numbers used to this JSON file")
194
+ ap.add_argument("--cache", default="out/http_cache")
195
+ ap.add_argument("--json", help="write the full results as JSON")
196
+ a = ap.parse_args(argv)
197
+ from .httpcache import CachedHTTP
198
+ http = CachedHTTP(a.cache, min_interval=0.3, offline=a.offline, ttl_s=30 * 86400)
199
+ fixture = json.loads(Path(a.fixture).read_text()) if a.fixture else None
200
+ results, summary = run(http=http, fixture=fixture)
201
+ print(report(results, summary))
202
+ if a.save_fixture:
203
+ keep = {}
204
+ for name, lib, _ in BENCH:
205
+ ref = reference(name, lib, http, fixture)
206
+ if ref:
207
+ keep[name] = ref
208
+ src = KICAD_RAW + " (numbers only: pad centre, size, drill; courtyard box). KiCad libraries are CC-BY-SA 4.0."
209
+ lines = [f"{json.dumps(k)}: {json.dumps(v, separators=(',', ':'))}" for k, v in {"_source": src, **keep}.items()]
210
+ Path(a.save_fixture).write_text("{\n" + ",\n".join(lines) + "\n}\n")
211
+ print(f"fixture: {len(keep)} footprints -> {a.save_fixture}")
212
+ if a.json:
213
+ Path(a.json).write_text(json.dumps({"summary": summary, "results": results}, indent=1))
214
+ return 0 if summary["footprints"] and not summary["unavailable"] and summary["pads_ok"] == summary["pads"] else 1
215
+
216
+
217
+ if __name__ == "__main__":
218
+ sys.exit(main())