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.
- ecadlib/__init__.py +3 -0
- ecadlib/__main__.py +5 -0
- ecadlib/a365_sync.py +637 -0
- ecadlib/agent.py +1407 -0
- ecadlib/agent_queue.py +780 -0
- ecadlib/agent_watch.py +732 -0
- ecadlib/altium365.py +385 -0
- ecadlib/altium_dblib_sync.py +530 -0
- ecadlib/altium_export.py +209 -0
- ecadlib/altium_forms.py +768 -0
- ecadlib/altium_lib.py +1089 -0
- ecadlib/altium_menu.py +1751 -0
- ecadlib/altium_pkg.py +396 -0
- ecadlib/altium_project.py +1095 -0
- ecadlib/app.py +1458 -0
- ecadlib/approvals.py +395 -0
- ecadlib/assets/demo_bom.csv +8 -0
- ecadlib/assets/kicad_action_plugin.py +607 -0
- ecadlib/assets/kicad_reread.py +60 -0
- ecadlib/assets/pcblib_template.json.gz +0 -0
- ecadlib/bom_inputs.py +339 -0
- ecadlib/bom_triage.py +519 -0
- ecadlib/cad_model.py +528 -0
- ecadlib/cfb.py +171 -0
- ecadlib/cli.py +185 -0
- ecadlib/clihelp.py +149 -0
- ecadlib/cliutil.py +119 -0
- ecadlib/completion.py +173 -0
- ecadlib/costs.py +276 -0
- ecadlib/crosscheck.py +412 -0
- ecadlib/datasheet.py +2168 -0
- ecadlib/dedupe.py +572 -0
- ecadlib/detect.py +170 -0
- ecadlib/doctor.py +450 -0
- ecadlib/ds_route.py +541 -0
- ecadlib/eagle_export.py +1409 -0
- ecadlib/easy_setup.py +338 -0
- ecadlib/easyeda_convert.py +386 -0
- ecadlib/easyeda_pro_export.py +972 -0
- ecadlib/eval/__init__.py +0 -0
- ecadlib/eval/baselines/kicad-v1.json +404 -0
- ecadlib/eval/build_set.py +299 -0
- ecadlib/eval/cli.py +119 -0
- ecadlib/eval/gate.py +358 -0
- ecadlib/eval/golden/demo.txt +379 -0
- ecadlib/eval/golden/specs.txt +3120 -0
- ecadlib/eval/golden/testpack.txt +463 -0
- ecadlib/eval/golden.py +446 -0
- ecadlib/eval/replay/kicad-v1.json.gz +0 -0
- ecadlib/eval/runner.py +456 -0
- ecadlib/eval/score.py +179 -0
- ecadlib/eval/sets/kicad-v1.json +630 -0
- ecadlib/firstrun.py +400 -0
- ecadlib/footprint_adv.py +1006 -0
- ecadlib/footprint_bench.py +218 -0
- ecadlib/footprint_check.py +562 -0
- ecadlib/footprint_gen.py +582 -0
- ecadlib/footprint_rules.py +1092 -0
- ecadlib/forge.py +267 -0
- ecadlib/fp_share.py +95 -0
- ecadlib/fp_tidy.py +389 -0
- ecadlib/fsutil.py +62 -0
- ecadlib/house_style.py +1051 -0
- ecadlib/httpcache.py +124 -0
- ecadlib/hub.py +1490 -0
- ecadlib/hub_copy.py +161 -0
- ecadlib/hub_tools.py +79 -0
- ecadlib/kicad_dblib.py +257 -0
- ecadlib/kicad_export.py +837 -0
- ecadlib/kicad_import.py +913 -0
- ecadlib/kicad_index.py +160 -0
- ecadlib/kicad_menu.py +193 -0
- ecadlib/kicad_pcm.py +475 -0
- ecadlib/launcher.py +156 -0
- ecadlib/librarian.py +963 -0
- ecadlib/lifecycle.py +691 -0
- ecadlib/llm/__init__.py +276 -0
- ecadlib/llm/cli.py +70 -0
- ecadlib/llm/openai_compat.py +214 -0
- ecadlib/llm/profiles.py +245 -0
- ecadlib/llm/rails.py +570 -0
- ecadlib/llm/run.py +126 -0
- ecadlib/llm/sampling.py +49 -0
- ecadlib/mcp_server.py +723 -0
- ecadlib/messages.py +419 -0
- ecadlib/migrate.py +407 -0
- ecadlib/notify.py +329 -0
- ecadlib/package_schema.json +1238 -0
- ecadlib/package_specs/CP_EIA-3216-18_Kemet-A.json +27 -0
- ecadlib/package_specs/C_0402_1005Metric.json +22 -0
- ecadlib/package_specs/C_0805_2012Metric.json +22 -0
- ecadlib/package_specs/DFN-8-1EP_3x3mm_P0.5mm_EP1.66x2.38mm.json +40 -0
- ecadlib/package_specs/D_SMA.json +27 -0
- ecadlib/package_specs/D_SMB.json +27 -0
- ecadlib/package_specs/D_SOD-123.json +43 -0
- ecadlib/package_specs/D_SOD-323.json +43 -0
- ecadlib/package_specs/LED_0603_1608Metric.json +23 -0
- ecadlib/package_specs/LED_0805_2012Metric.json +23 -0
- ecadlib/package_specs/LQFP-48_7x7mm_P0.5mm.json +31 -0
- ecadlib/package_specs/L_0805_2012Metric.json +22 -0
- ecadlib/package_specs/MSOP-8_3x3mm_P0.65mm.json +31 -0
- ecadlib/package_specs/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm.json +39 -0
- ecadlib/package_specs/QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm.json +37 -0
- ecadlib/package_specs/R_0402_1005Metric.json +22 -0
- ecadlib/package_specs/R_0603_1608Metric.json +22 -0
- ecadlib/package_specs/R_0805_2012Metric.json +22 -0
- ecadlib/package_specs/R_1206_3216Metric.json +22 -0
- ecadlib/package_specs/SOIC-16_3.9x9.9mm_P1.27mm.json +32 -0
- ecadlib/package_specs/SOIC-8_3.9x4.9mm_P1.27mm.json +32 -0
- ecadlib/package_specs/SOT-223-3_TabPin2.json +56 -0
- ecadlib/package_specs/SOT-23-5.json +57 -0
- ecadlib/package_specs/SOT-23-6.json +62 -0
- ecadlib/package_specs/SOT-23.json +48 -0
- ecadlib/package_specs/TSSOP-16_4.4x5mm_P0.65mm.json +31 -0
- ecadlib/package_specs/VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm.json +39 -0
- ecadlib/package_specs/advanced/BGA-100_11.0x11.0mm_Layout10x10_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-121_9.0x9.0mm_Layout11x11_P0.8mm_Ball0.4mm_Pad0.35mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-144_7.0x7.0mm_Layout13x13_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +29 -0
- ecadlib/package_specs/advanced/BGA-169_11.0x11.0mm_Layout13x13_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +23 -0
- ecadlib/package_specs/advanced/BGA-256_11.0x11.0mm_Layout20x20_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-256_17.0x17.0mm_Layout16x16_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-324_15.0x15.0mm_Layout18x18_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-36_3.396x3.466mm_Layout6x6_P0.4mm_Ball0.25mm_Pad0.2mm_NSMD.json +24 -0
- ecadlib/package_specs/advanced/BGA-672_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD.json +32 -0
- ecadlib/package_specs/advanced/BGA-68_5.0x5.0mm_Layout9x9_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +30 -0
- ecadlib/package_specs/advanced/BGA-90_8.0x13.0mm_Layout2x3x15_P0.8mm.json +26 -0
- ecadlib/package_specs/advanced/DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias.json +60 -0
- ecadlib/package_specs/advanced/Example_Custom_LPad_Slot.json +24 -0
- ecadlib/package_specs/advanced/JST_GH_SM04B-GHS-TB_1x04-1MP_P1.25mm_Horizontal.json +49 -0
- ecadlib/package_specs/advanced/JST_PH_B4B-PH-K_1x04_P2.00mm_Vertical.json +33 -0
- ecadlib/package_specs/advanced/JST_PH_S4B-PH-SM4-TB_1x04-1MP_P2.00mm_Horizontal.json +55 -0
- ecadlib/package_specs/advanced/JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal.json +49 -0
- ecadlib/package_specs/advanced/JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/JST_XH_B6B-XH-A_1x06_P2.50mm_Vertical.json +33 -0
- ecadlib/package_specs/advanced/LFBGA-144_10x10mm_Layout12x12_P0.8mm.json +23 -0
- ecadlib/package_specs/advanced/Molex_KK-254_AE-6410-04A_1x04_P2.54mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/Molex_PicoBlade_53398-0471_1x04-1MP_P1.25mm_Vertical.json +54 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P1.00mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P1.27mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Left.json +25 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Right.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x10_P1.00mm_Vertical.json +15 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.00mm_Vertical_SMD.json +13 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical.json +18 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical_SMD.json +24 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.00mm_Vertical_SMD.json +13 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical_SMD.json +24 -0
- ecadlib/package_specs/advanced/PinHeader_2x20_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm_ThermalVias.json +87 -0
- ecadlib/package_specs/advanced/UFBGA-132_7x7mm_Layout12x12_P0.5mm.json +43 -0
- ecadlib/package_specs/advanced/USB_C_Receptacle_HRO_TYPE-C-31-M-12.json +218 -0
- ecadlib/package_specs/standard.json.gz +0 -0
- ecadlib/parity.py +1204 -0
- ecadlib/parts_export.py +154 -0
- ecadlib/partview.py +215 -0
- ecadlib/plugins.py +231 -0
- ecadlib/providers.py +226 -0
- ecadlib/rebuild.py +471 -0
- ecadlib/release_check.py +437 -0
- ecadlib/reports.py +274 -0
- ecadlib/resolve.py +556 -0
- ecadlib/review_cli.py +292 -0
- ecadlib/rules.py +979 -0
- ecadlib/rules_infer.py +1205 -0
- ecadlib/sexpr.py +149 -0
- ecadlib/sheetsafe.py +65 -0
- ecadlib/sim_models.py +1087 -0
- ecadlib/sourcing.py +1161 -0
- ecadlib/stdparts.py +755 -0
- ecadlib/step_gen.py +929 -0
- ecadlib/symbol_layout.py +798 -0
- ecadlib/team.py +558 -0
- ecadlib/team_setup.py +288 -0
- ecadlib/term.py +420 -0
- ecadlib/term_views.py +64 -0
- ecadlib/testpack.py +334 -0
- ecadlib/ui/__init__.py +1 -0
- ecadlib/ui/__main__.py +3 -0
- ecadlib/ui/server.py +1029 -0
- ecadlib/ui/static/hairline.js +2 -0
- ecadlib/ui/static/index.html +2098 -0
- ecadlib/ui/static/setup.html +294 -0
- ecadlib/upkeep.py +717 -0
- ecadlib/upstream.py +573 -0
- ecadlib/userconfig.py +257 -0
- ecadlib/visual_check.py +1603 -0
- ecadlib/ws_status.py +203 -0
- ecadlib-0.3.0.dist-info/METADATA +2245 -0
- ecadlib-0.3.0.dist-info/RECORD +199 -0
- ecadlib-0.3.0.dist-info/WHEEL +5 -0
- ecadlib-0.3.0.dist-info/entry_points.txt +2 -0
- ecadlib-0.3.0.dist-info/licenses/LICENSE +21 -0
- ecadlib-0.3.0.dist-info/licenses/NOTICE +53 -0
- 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())
|