gitcad 0.7.7__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.
- gitcad/bench/__init__.py +0 -0
- gitcad/bench/corpus.py +260 -0
- gitcad/bench/scorecard.py +176 -0
- gitcad/bridge.py +72 -0
- gitcad/convert.py +30 -0
- gitcad/explore.py +99 -0
- gitcad/fasteners.py +112 -0
- gitcad/init.py +134 -0
- gitcad/lots.py +136 -0
- gitcad/mcp/__init__.py +15 -0
- gitcad/mcp/server.py +1065 -0
- gitcad/merge3.py +289 -0
- gitcad/pcba.py +114 -0
- gitcad/release.py +223 -0
- gitcad/render.py +177 -0
- gitcad/requirements.py +204 -0
- gitcad/review.py +230 -0
- gitcad/viewer/__init__.py +19 -0
- gitcad/viewer/boardsvg.py +80 -0
- gitcad/viewer/page.py +577 -0
- gitcad/viewer/server.py +433 -0
- gitcad-0.7.7.dist-info/METADATA +70 -0
- gitcad-0.7.7.dist-info/RECORD +25 -0
- gitcad-0.7.7.dist-info/WHEEL +4 -0
- gitcad-0.7.7.dist-info/entry_points.txt +11 -0
gitcad/bench/__init__.py
ADDED
|
File without changes
|
gitcad/bench/corpus.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""The kernel benchmark corpus — deterministic documents per operator
|
|
2
|
+
class (ADR-0018 W0).
|
|
3
|
+
|
|
4
|
+
Each entry builds a Document tagged with the operator classes it
|
|
5
|
+
exercises. This corpus is simultaneously: the differential-test input
|
|
6
|
+
(ref vs occt), the benchmark workload (timed per backend), and the
|
|
7
|
+
future acceptance suite for kernel cutover. Torture entries encode the
|
|
8
|
+
configurations that break kernels — tangencies, coincidences, slivers
|
|
9
|
+
— where beating OCCT must become measurable.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Callable
|
|
15
|
+
|
|
16
|
+
from gitcad.document import Document, Feature
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def plate_with_holes() -> Document:
|
|
20
|
+
d = Document()
|
|
21
|
+
base = d.add(Feature(op="box", params={"dx": 60, "dy": 40, "dz": 4}))
|
|
22
|
+
prev = base
|
|
23
|
+
for i in range(4):
|
|
24
|
+
prev = d.add(Feature(op="hole", params={
|
|
25
|
+
"x": 10 + 13 * i, "y": 20, "top_z": 4, "depth": 4,
|
|
26
|
+
"diameter": 5}, inputs=[prev]))
|
|
27
|
+
return d
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def quadric_boss() -> Document:
|
|
31
|
+
d = Document()
|
|
32
|
+
base = d.add(Feature(op="cylinder", params={"radius": 20, "height": 8}))
|
|
33
|
+
boss = d.add(Feature(op="cone", params={"r1": 10, "r2": 6, "height": 12}))
|
|
34
|
+
bm = d.add(Feature(op="move", params={"translate": [0, 0, 8]}, inputs=[boss]))
|
|
35
|
+
u = d.add(Feature(op="boolean", params={"kind": "union"}, inputs=[base, bm]))
|
|
36
|
+
s = d.add(Feature(op="sphere", params={"radius": 5}))
|
|
37
|
+
sm = d.add(Feature(op="move", params={"translate": [0, 0, 22]}, inputs=[s]))
|
|
38
|
+
d.add(Feature(op="boolean", params={"kind": "union"}, inputs=[u, sm]))
|
|
39
|
+
return d
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def revolve_profile() -> Document:
|
|
43
|
+
d = Document()
|
|
44
|
+
profile = {"start": [5, 0], "segments": [
|
|
45
|
+
{"kind": "line", "to": [15, 0]}, {"kind": "line", "to": [15, 4]},
|
|
46
|
+
{"kind": "line", "to": [8, 8]}, {"kind": "line", "to": [8, 20]},
|
|
47
|
+
{"kind": "line", "to": [5, 20]}, {"kind": "line", "to": [5, 0]}]}
|
|
48
|
+
d.add(Feature(op="revolve", params={"profile": profile, "angle_deg": 360.0}))
|
|
49
|
+
return d
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def extrude_L() -> Document:
|
|
53
|
+
d = Document()
|
|
54
|
+
profile = {"start": [0, 0], "segments": [
|
|
55
|
+
{"kind": "line", "to": [30, 0]}, {"kind": "line", "to": [30, 8]},
|
|
56
|
+
{"kind": "line", "to": [8, 8]}, {"kind": "line", "to": [8, 25]},
|
|
57
|
+
{"kind": "line", "to": [0, 25]}, {"kind": "line", "to": [0, 0]}]}
|
|
58
|
+
d.add(Feature(op="extrude", params={"profile": profile, "height": 12}))
|
|
59
|
+
return d
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def filleted_block() -> Document:
|
|
63
|
+
d = Document()
|
|
64
|
+
b = d.add(Feature(op="box", params={"dx": 30, "dy": 20, "dz": 10}))
|
|
65
|
+
d.add(Feature(op="fillet", params={"radius": 2.5}, inputs=[b]))
|
|
66
|
+
return d
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def chamfered_block() -> Document:
|
|
70
|
+
d = Document()
|
|
71
|
+
b = d.add(Feature(op="box", params={"dx": 30, "dy": 20, "dz": 10}))
|
|
72
|
+
d.add(Feature(op="chamfer", params={"distance": 2.0}, inputs=[b]))
|
|
73
|
+
return d
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def shelled_box() -> Document:
|
|
77
|
+
d = Document()
|
|
78
|
+
b = d.add(Feature(op="box", params={"dx": 40, "dy": 30, "dz": 20}))
|
|
79
|
+
d.add(Feature(op="shell", params={"faces": [], "thickness": 2.0},
|
|
80
|
+
inputs=[b]))
|
|
81
|
+
return d
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def shelled_prism() -> Document:
|
|
85
|
+
# K4.1: closed hollow of a non-box prism — a right trapezoid with a
|
|
86
|
+
# 3-4-5 hypotenuse (Pythagorean edges → the exact inward inset).
|
|
87
|
+
# ref = exactly 541/6; OCCT's thick-solid offset agrees in float.
|
|
88
|
+
d = Document()
|
|
89
|
+
p = d.add(Feature(op="extrude", params={"profile": {
|
|
90
|
+
"start": [0, 0], "segments": [
|
|
91
|
+
{"kind": "line", "to": [8, 0]}, {"kind": "line", "to": [8, 3]},
|
|
92
|
+
{"kind": "line", "to": [4, 6]}, {"kind": "line", "to": [0, 6]},
|
|
93
|
+
{"kind": "line", "to": [0, 0]}]}, "height": 5}))
|
|
94
|
+
d.add(Feature(op="shell", params={"faces": [], "thickness": 0.5},
|
|
95
|
+
inputs=[p]))
|
|
96
|
+
return d
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def drafted_block() -> Document:
|
|
100
|
+
d = Document()
|
|
101
|
+
b = d.add(Feature(op="box", params={"dx": 30, "dy": 30, "dz": 15}))
|
|
102
|
+
d.add(Feature(op="draft", params={"faces": [], "angle_deg": 3.0},
|
|
103
|
+
inputs=[b]))
|
|
104
|
+
return d
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def spring() -> Document:
|
|
108
|
+
d = Document()
|
|
109
|
+
d.add(Feature(op="spring", params={"radius": 8, "pitch": 4, "turns": 6,
|
|
110
|
+
"wire_diameter": 1.5}))
|
|
111
|
+
return d
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def swept_channel() -> Document:
|
|
115
|
+
d = Document()
|
|
116
|
+
profile = {"start": [-2, -2], "segments": [
|
|
117
|
+
{"kind": "line", "to": [2, -2]}, {"kind": "line", "to": [2, 2]},
|
|
118
|
+
{"kind": "line", "to": [-2, 2]}, {"kind": "line", "to": [-2, -2]}]}
|
|
119
|
+
d.add(Feature(op="sweep", params={"profile": profile,
|
|
120
|
+
"path": [[0, 0, 0], [0, 0, 20],
|
|
121
|
+
[15, 0, 35], [40, 0, 35]]}))
|
|
122
|
+
return d
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def loft_transition() -> Document:
|
|
126
|
+
d = Document()
|
|
127
|
+
sq = {"start": [-10, -10], "segments": [
|
|
128
|
+
{"kind": "line", "to": [10, -10]}, {"kind": "line", "to": [10, 10]},
|
|
129
|
+
{"kind": "line", "to": [-10, 10]}, {"kind": "line", "to": [-10, -10]}]}
|
|
130
|
+
sm = {"start": [-4, -4], "segments": [
|
|
131
|
+
{"kind": "line", "to": [4, -4]}, {"kind": "line", "to": [4, 4]},
|
|
132
|
+
{"kind": "line", "to": [-4, 4]}, {"kind": "line", "to": [-4, -4]}]}
|
|
133
|
+
d.add(Feature(op="loft", params={"sections": [
|
|
134
|
+
{"profile": sq, "z": 0.0}, {"profile": sm, "z": 25.0}]}))
|
|
135
|
+
return d
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def sheetmetal_folded() -> Document:
|
|
139
|
+
from gitcad.sheetmetal import Flange, SheetMetal
|
|
140
|
+
|
|
141
|
+
sm = SheetMetal(name="bench-bracket", width=40, height=50, thickness=2,
|
|
142
|
+
k_factor=0.44, bend_radius=2,
|
|
143
|
+
flanges=[Flange(edge="n", length=20),
|
|
144
|
+
Flange(edge="e", length=12, direction="down")])
|
|
145
|
+
return sm.to_document()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# -- torture: the configurations that break kernels ---------------------------
|
|
149
|
+
|
|
150
|
+
def torture_tangent_cylinders() -> Document:
|
|
151
|
+
d = Document()
|
|
152
|
+
a = d.add(Feature(op="cylinder", params={"radius": 10, "height": 10}))
|
|
153
|
+
b = d.add(Feature(op="cylinder", params={"radius": 10, "height": 10}))
|
|
154
|
+
bm = d.add(Feature(op="move", params={"translate": [20, 0, 0]}, inputs=[b]))
|
|
155
|
+
d.add(Feature(op="boolean", params={"kind": "union"}, inputs=[a, bm]))
|
|
156
|
+
return d
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def torture_coincident_faces() -> Document:
|
|
160
|
+
d = Document()
|
|
161
|
+
a = d.add(Feature(op="box", params={"dx": 20, "dy": 20, "dz": 10}))
|
|
162
|
+
b = d.add(Feature(op="box", params={"dx": 20, "dy": 20, "dz": 10}))
|
|
163
|
+
bm = d.add(Feature(op="move", params={"translate": [20, 0, 0]}, inputs=[b]))
|
|
164
|
+
d.add(Feature(op="boolean", params={"kind": "union"}, inputs=[a, bm]))
|
|
165
|
+
return d
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def torture_sliver_cut() -> Document:
|
|
169
|
+
d = Document()
|
|
170
|
+
a = d.add(Feature(op="box", params={"dx": 30, "dy": 30, "dz": 10}))
|
|
171
|
+
b = d.add(Feature(op="box", params={"dx": 30, "dy": 30, "dz": 10}))
|
|
172
|
+
bm = d.add(Feature(op="move", params={"translate": [29.999, 0, 0]},
|
|
173
|
+
inputs=[b]))
|
|
174
|
+
d.add(Feature(op="boolean", params={"kind": "cut"}, inputs=[a, bm]))
|
|
175
|
+
return d
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def torture_tangent_sphere_plane() -> Document:
|
|
179
|
+
d = Document()
|
|
180
|
+
a = d.add(Feature(op="box", params={"dx": 30, "dy": 30, "dz": 10}))
|
|
181
|
+
s = d.add(Feature(op="sphere", params={"radius": 5}))
|
|
182
|
+
sm = d.add(Feature(op="move", params={"translate": [15, 15, 15]},
|
|
183
|
+
inputs=[s]))
|
|
184
|
+
d.add(Feature(op="boolean", params={"kind": "union"}, inputs=[a, sm]))
|
|
185
|
+
return d
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def quadric_sphere_overlap() -> Document:
|
|
189
|
+
"""Two GENUINELY OVERLAPPING spheres unioned — a non-coaxial quadric
|
|
190
|
+
boolean OCCT builds and ref does EXACTLY in ℚ[π] (cap/lens): r=5
|
|
191
|
+
spheres 6 apart -> union = 896/3 π. The K2.2 exact case (parallel-
|
|
192
|
+
cylinder overlap stays transcendental / refused)."""
|
|
193
|
+
d = Document()
|
|
194
|
+
a = d.add(Feature(op="sphere", params={"radius": 5}))
|
|
195
|
+
b = d.add(Feature(op="sphere", params={"radius": 5}))
|
|
196
|
+
bm = d.add(Feature(op="move", params={"translate": [6, 0, 0]}, inputs=[b]))
|
|
197
|
+
d.add(Feature(op="boolean", params={"kind": "union"}, inputs=[a, bm]))
|
|
198
|
+
return d
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def sweep_rightangle() -> Document:
|
|
202
|
+
"""A square profile swept along a 90-degree-cornered axis-aligned path
|
|
203
|
+
— all segment lengths RATIONAL, so ref builds it in plain Q (no surd
|
|
204
|
+
field needed). Exact volume = area * (10+10+10) = 16*30 = 480."""
|
|
205
|
+
d = Document()
|
|
206
|
+
profile = {"start": [-2, -2], "segments": [
|
|
207
|
+
{"kind": "line", "to": [2, -2]}, {"kind": "line", "to": [2, 2]},
|
|
208
|
+
{"kind": "line", "to": [-2, 2]}, {"kind": "line", "to": [-2, -2]}]}
|
|
209
|
+
d.add(Feature(op="sweep", params={"profile": profile,
|
|
210
|
+
"path": [[0, 0, 0], [0, 0, 10],
|
|
211
|
+
[10, 0, 10], [10, 0, 20]]}))
|
|
212
|
+
return d
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def torture_menger_1() -> Document:
|
|
216
|
+
"""The K1 acid test: Menger sponge level 1 — a 27mm cube minus three
|
|
217
|
+
orthogonal 9mm square rods through the center. Five chained booleans
|
|
218
|
+
with coincident internal faces everywhere; exact volume is KNOWN:
|
|
219
|
+
20/27 of the cube = 14580 mm^3. A kernel that smears tolerances gets
|
|
220
|
+
this wrong; an exact kernel gets it to the last bit."""
|
|
221
|
+
d = Document()
|
|
222
|
+
cube = d.add(Feature(op="box", params={"dx": 27, "dy": 27, "dz": 27}))
|
|
223
|
+
rz = d.add(Feature(op="box", params={"dx": 9, "dy": 9, "dz": 27}))
|
|
224
|
+
rzm = d.add(Feature(op="move", params={"translate": [9, 9, 0]}, inputs=[rz]))
|
|
225
|
+
rx = d.add(Feature(op="box", params={"dx": 27, "dy": 9, "dz": 9}))
|
|
226
|
+
rxm = d.add(Feature(op="move", params={"translate": [0, 9, 9]}, inputs=[rx]))
|
|
227
|
+
ry = d.add(Feature(op="box", params={"dx": 9, "dy": 27, "dz": 9}))
|
|
228
|
+
rym = d.add(Feature(op="move", params={"translate": [9, 0, 9]}, inputs=[ry]))
|
|
229
|
+
u1 = d.add(Feature(op="boolean", params={"kind": "union"}, inputs=[rzm, rxm]))
|
|
230
|
+
u2 = d.add(Feature(op="boolean", params={"kind": "union"}, inputs=[u1, rym]))
|
|
231
|
+
d.add(Feature(op="boolean", params={"kind": "cut"}, inputs=[cube, u2]))
|
|
232
|
+
return d
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
CORPUS: list[tuple[str, tuple[str, ...], Callable[[], Document]]] = [
|
|
236
|
+
("plate_with_holes", ("planar", "boolean", "hole"), plate_with_holes),
|
|
237
|
+
("quadric_boss", ("quadric", "boolean"), quadric_boss),
|
|
238
|
+
("revolve_profile", ("quadric", "revolve"), revolve_profile),
|
|
239
|
+
("extrude_L", ("planar", "extrude"), extrude_L),
|
|
240
|
+
("filleted_block", ("blend",), filleted_block),
|
|
241
|
+
("chamfered_block", ("planar", "blend"), chamfered_block),
|
|
242
|
+
("shelled_box", ("offset",), shelled_box),
|
|
243
|
+
("shelled_prism", ("offset",), shelled_prism),
|
|
244
|
+
("drafted_block", ("draft",), drafted_block),
|
|
245
|
+
("spring", ("sweep", "freeform"), spring),
|
|
246
|
+
("swept_channel", ("sweep",), swept_channel),
|
|
247
|
+
("loft_transition", ("loft", "freeform"), loft_transition),
|
|
248
|
+
("sheetmetal_folded", ("planar", "boolean"), sheetmetal_folded),
|
|
249
|
+
("torture_tangent_cylinders", ("torture", "quadric", "boolean"),
|
|
250
|
+
torture_tangent_cylinders),
|
|
251
|
+
("torture_coincident_faces", ("torture", "planar", "boolean"),
|
|
252
|
+
torture_coincident_faces),
|
|
253
|
+
("torture_sliver_cut", ("torture", "planar", "boolean"),
|
|
254
|
+
torture_sliver_cut),
|
|
255
|
+
("torture_tangent_sphere_plane", ("torture", "quadric", "boolean"),
|
|
256
|
+
torture_tangent_sphere_plane),
|
|
257
|
+
("torture_menger_1", ("torture", "planar", "boolean"), torture_menger_1),
|
|
258
|
+
("sweep_rightangle", ("sweep",), sweep_rightangle),
|
|
259
|
+
("quadric_sphere_overlap", ("quadric", "boolean"), quadric_sphere_overlap),
|
|
260
|
+
]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Kernel scorecard — the benchmark that makes improvement provable
|
|
2
|
+
(ADR-0018 W0; owner requirement: "show we are actually improving").
|
|
3
|
+
|
|
4
|
+
Runs the corpus on a named backend and emits a snapshot: per-model
|
|
5
|
+
build result, wall time, volume/bbox/topology metrics, validation.
|
|
6
|
+
With two backends, emits deltas. Snapshots are dated JSON committed
|
|
7
|
+
under bench/; TREND.md regenerates from them so every improvement
|
|
8
|
+
claim has a number in git history behind it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from gitcad.bench.corpus import CORPUS
|
|
19
|
+
|
|
20
|
+
_REL_TOL = 1e-9
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_kernel(name: str):
|
|
24
|
+
if name == "occt":
|
|
25
|
+
from gitcad.kernel.occt import OcctKernel
|
|
26
|
+
return OcctKernel()
|
|
27
|
+
if name == "null":
|
|
28
|
+
from gitcad.kernel.null import NullKernel
|
|
29
|
+
return NullKernel()
|
|
30
|
+
if name == "ref":
|
|
31
|
+
from gitcad.kernel.ref import RefKernel
|
|
32
|
+
return RefKernel()
|
|
33
|
+
if name == "forge":
|
|
34
|
+
from gitcad.kernel.rustref import RustKernel
|
|
35
|
+
return RustKernel()
|
|
36
|
+
if name == "auto":
|
|
37
|
+
from gitcad.kernel.auto import AutoKernel
|
|
38
|
+
return AutoKernel()
|
|
39
|
+
raise ValueError(f"unknown backend {name!r} (occt|null|ref|forge|auto)")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def run_backend(backend: str) -> dict[str, Any]:
|
|
43
|
+
"""Build every corpus model on one backend; never raises — failures
|
|
44
|
+
are data (that is the point of a robustness benchmark)."""
|
|
45
|
+
kernel = _get_kernel(backend)
|
|
46
|
+
models: dict[str, Any] = {}
|
|
47
|
+
for name, classes, build in CORPUS:
|
|
48
|
+
entry: dict[str, Any] = {"classes": list(classes)}
|
|
49
|
+
t0 = time.perf_counter()
|
|
50
|
+
try:
|
|
51
|
+
doc = build()
|
|
52
|
+
result = doc.build(kernel)
|
|
53
|
+
shape = result.final(doc)
|
|
54
|
+
entry["ok"] = True
|
|
55
|
+
entry["seconds"] = round(time.perf_counter() - t0, 4)
|
|
56
|
+
try:
|
|
57
|
+
mp = kernel.mass_props(shape)
|
|
58
|
+
entry["volume"] = mp.get("volume")
|
|
59
|
+
except Exception as exc: # metric, not fatal
|
|
60
|
+
entry["volume_error"] = f"{type(exc).__name__}: {exc}"
|
|
61
|
+
try:
|
|
62
|
+
(x0, y0, z0), (x1, y1, z1) = kernel.bbox(shape)
|
|
63
|
+
entry["bbox"] = [round(v, 6) for v in
|
|
64
|
+
(x0, y0, z0, x1, y1, z1)]
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
try:
|
|
68
|
+
entry["faces"] = len(kernel.entities(shape, "face"))
|
|
69
|
+
entry["edges"] = len(kernel.entities(shape, "edge"))
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
try:
|
|
73
|
+
v = kernel.validate(shape)
|
|
74
|
+
entry["valid"] = bool(v.ok)
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
except Exception as exc:
|
|
78
|
+
entry["ok"] = False
|
|
79
|
+
entry["seconds"] = round(time.perf_counter() - t0, 4)
|
|
80
|
+
entry["error"] = f"{type(exc).__name__}: {exc}"
|
|
81
|
+
models[name] = entry
|
|
82
|
+
|
|
83
|
+
by_class: dict[str, dict[str, int]] = {}
|
|
84
|
+
for name, entry in models.items():
|
|
85
|
+
for cls in entry["classes"]:
|
|
86
|
+
c = by_class.setdefault(cls, {"total": 0, "ok": 0, "valid": 0})
|
|
87
|
+
c["total"] += 1
|
|
88
|
+
c["ok"] += 1 if entry.get("ok") else 0
|
|
89
|
+
c["valid"] += 1 if entry.get("valid") else 0
|
|
90
|
+
return {"backend": backend, "models": models, "by_class": by_class,
|
|
91
|
+
"capability_pct": round(
|
|
92
|
+
100 * sum(1 for m in models.values() if m.get("ok"))
|
|
93
|
+
/ len(models), 1)}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def compare(a: dict[str, Any], b: dict[str, Any]) -> dict[str, Any]:
|
|
97
|
+
"""Deltas of run b against oracle run a (volume rel-delta, topology
|
|
98
|
+
count diffs, disagreement list)."""
|
|
99
|
+
out: dict[str, Any] = {"oracle": a["backend"], "subject": b["backend"],
|
|
100
|
+
"models": {}, "disagreements": []}
|
|
101
|
+
for name, ea in a["models"].items():
|
|
102
|
+
eb = b["models"].get(name, {})
|
|
103
|
+
row: dict[str, Any] = {"oracle_ok": ea.get("ok"),
|
|
104
|
+
"subject_ok": eb.get("ok")}
|
|
105
|
+
va, vb = ea.get("volume"), eb.get("volume")
|
|
106
|
+
if isinstance(va, (int, float)) and isinstance(vb, (int, float)) and va:
|
|
107
|
+
row["volume_rel_delta"] = abs(vb - va) / abs(va)
|
|
108
|
+
if row["volume_rel_delta"] > 1e-6:
|
|
109
|
+
out["disagreements"].append(
|
|
110
|
+
f"{name}: volume {va:.6g} vs {vb:.6g}")
|
|
111
|
+
if ea.get("ok") != eb.get("ok"):
|
|
112
|
+
out["disagreements"].append(
|
|
113
|
+
f"{name}: ok {ea.get('ok')} vs {eb.get('ok')}")
|
|
114
|
+
for k in ("faces", "edges"):
|
|
115
|
+
if k in ea and k in eb and ea[k] != eb[k]:
|
|
116
|
+
row[f"{k}_diff"] = (ea[k], eb[k])
|
|
117
|
+
out["models"][name] = row
|
|
118
|
+
return out
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def snapshot(backends: list[str], outdir: str = "bench", *,
|
|
122
|
+
stamp: str) -> dict[str, Any]:
|
|
123
|
+
"""Run + persist one dated benchmark snapshot; regenerate TREND.md."""
|
|
124
|
+
runs = {b: run_backend(b) for b in backends}
|
|
125
|
+
payload: dict[str, Any] = {"stamp": stamp, "runs": runs}
|
|
126
|
+
if len(backends) == 2:
|
|
127
|
+
payload["compare"] = compare(runs[backends[0]], runs[backends[1]])
|
|
128
|
+
out = Path(outdir)
|
|
129
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
path = out / f"{stamp}-{'-vs-'.join(backends)}.json"
|
|
131
|
+
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n",
|
|
132
|
+
newline="\n")
|
|
133
|
+
_render_trend(out)
|
|
134
|
+
return payload
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _render_trend(outdir: Path) -> None:
|
|
138
|
+
rows: list[str] = ["# Kernel benchmark trend", "",
|
|
139
|
+
"Regenerated from bench/*.json — every improvement "
|
|
140
|
+
"claim has a snapshot behind it.", "",
|
|
141
|
+
"| snapshot | backend | capability % | torture ok | "
|
|
142
|
+
"models ok | total s |",
|
|
143
|
+
"|---|---|---|---|---|---|"]
|
|
144
|
+
for p in sorted(outdir.glob("*.json")):
|
|
145
|
+
data = json.loads(p.read_text())
|
|
146
|
+
for backend, run in sorted(data.get("runs", {}).items()):
|
|
147
|
+
models = run["models"]
|
|
148
|
+
torture = [m for m in models.values() if "torture" in m["classes"]]
|
|
149
|
+
rows.append(
|
|
150
|
+
f"| {data['stamp']} | {backend} | {run['capability_pct']} | "
|
|
151
|
+
f"{sum(1 for m in torture if m.get('ok'))}/{len(torture)} | "
|
|
152
|
+
f"{sum(1 for m in models.values() if m.get('ok'))}/{len(models)} | "
|
|
153
|
+
f"{round(sum(m.get('seconds', 0) for m in models.values()), 2)} |")
|
|
154
|
+
(outdir / "TREND.md").write_text("\n".join(rows) + "\n", newline="\n")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main() -> None: # pragma: no cover - CLI entry
|
|
158
|
+
import argparse
|
|
159
|
+
from datetime import date
|
|
160
|
+
|
|
161
|
+
ap = argparse.ArgumentParser(description="kernel benchmark scorecard")
|
|
162
|
+
ap.add_argument("backends", nargs="+", help="occt | null | ref")
|
|
163
|
+
ap.add_argument("--outdir", default="bench")
|
|
164
|
+
ap.add_argument("--stamp", default=str(date.today()))
|
|
165
|
+
args = ap.parse_args()
|
|
166
|
+
payload = snapshot(args.backends, args.outdir, stamp=args.stamp)
|
|
167
|
+
for b, run in payload["runs"].items():
|
|
168
|
+
print(f"{b}: capability {run['capability_pct']}% "
|
|
169
|
+
f"({sum(1 for m in run['models'].values() if m.get('ok'))}"
|
|
170
|
+
f"/{len(run['models'])} models)")
|
|
171
|
+
for d in payload.get("compare", {}).get("disagreements", []):
|
|
172
|
+
print("DISAGREE:", d)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
main()
|
gitcad/bridge.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Cross-domain bridges (metapackage — the one place mech and ecad meet).
|
|
2
|
+
|
|
3
|
+
:func:`board_to_model` — the dogfood's #2 friction finding: interference
|
|
4
|
+
checking needed the board as real 3D geometry, previously hand-extruded.
|
|
5
|
+
Now: outline (polygon, any shape) x thickness, mounting holes cut through —
|
|
6
|
+
a mech Document derived from the ecad source, ready for assemblies,
|
|
7
|
+
interference, STEP export, or the viewer.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from gitcad.document import Document, Feature
|
|
13
|
+
from gitcad.ecad.board import Board
|
|
14
|
+
from gitcad.sketch import Profile
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def component_envelope(comp, *, default_height: float = 2.0
|
|
18
|
+
) -> tuple[float, float, float] | None:
|
|
19
|
+
"""(w, h, height) of a component's IDF-style envelope, rotation applied:
|
|
20
|
+
courtyard when declared, pads' extent + margin otherwise, None when the
|
|
21
|
+
footprint has neither."""
|
|
22
|
+
cy = comp.footprint.courtyard
|
|
23
|
+
if cy is not None:
|
|
24
|
+
cw, ch = cy
|
|
25
|
+
else:
|
|
26
|
+
pads = comp.footprint.pads
|
|
27
|
+
if not pads:
|
|
28
|
+
return None
|
|
29
|
+
cw = max(abs(p.x) + p.w / 2 for p in pads) * 2 + 0.4
|
|
30
|
+
ch = max(abs(p.y) + p.h / 2 for p in pads) * 2 + 0.4
|
|
31
|
+
if round(comp.rot) % 180 == 90:
|
|
32
|
+
cw, ch = ch, cw
|
|
33
|
+
return (cw, ch, comp.footprint.height or default_height)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def board_to_model(board: Board, *, components: bool = True,
|
|
37
|
+
default_height: float = 2.0) -> Document:
|
|
38
|
+
"""The board as a 3D body: extruded outline with mounting holes cut,
|
|
39
|
+
plus IDF-style component envelopes — each part's courtyard extruded to
|
|
40
|
+
its footprint height (``default_height`` when unknown). That makes the
|
|
41
|
+
populated board a REAL mechanical envelope: enclosure interference
|
|
42
|
+
checks see the tall electrolytic, not a bare slab."""
|
|
43
|
+
pts = list(board.outline)
|
|
44
|
+
prof = Profile(tuple(pts[0]))
|
|
45
|
+
for x, y in pts[1:]:
|
|
46
|
+
prof.line_to(x, y)
|
|
47
|
+
prof.close()
|
|
48
|
+
|
|
49
|
+
doc = Document()
|
|
50
|
+
fid = doc.add(Feature(op="extrude", params={"profile": prof.to_params(),
|
|
51
|
+
"height": board.thickness}))
|
|
52
|
+
for mh in board.mounting_holes:
|
|
53
|
+
fid = doc.add(Feature(op="hole", params={
|
|
54
|
+
"x": mh.x, "y": mh.y, "top_z": board.thickness,
|
|
55
|
+
"diameter": mh.drill, "depth": board.thickness}, inputs=[fid]))
|
|
56
|
+
if components:
|
|
57
|
+
for comp in board.components:
|
|
58
|
+
env = component_envelope(comp, default_height=default_height)
|
|
59
|
+
if env is None:
|
|
60
|
+
continue
|
|
61
|
+
cw, ch, h = env
|
|
62
|
+
# top: sits on the board surface; bottom: hangs below z=0
|
|
63
|
+
base = board.thickness if comp.side == "top" else -h
|
|
64
|
+
body = Profile((comp.x - cw / 2, comp.y - ch / 2)) \
|
|
65
|
+
.line_to(comp.x + cw / 2, comp.y - ch / 2) \
|
|
66
|
+
.line_to(comp.x + cw / 2, comp.y + ch / 2) \
|
|
67
|
+
.line_to(comp.x - cw / 2, comp.y + ch / 2).close()
|
|
68
|
+
fid = doc.add(Feature(op="extrude", params={
|
|
69
|
+
"profile": body.to_params(), "height": h,
|
|
70
|
+
"plane": {"offset": base},
|
|
71
|
+
"mode": "add"}, inputs=[fid]))
|
|
72
|
+
return doc
|
gitcad/convert.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""gitcad-convert — one-command onboarding from other CAD.
|
|
2
|
+
|
|
3
|
+
Currently: multi-body FreeCAD .FCStd -> a full gitcad project (per-body
|
|
4
|
+
.model + .part, content-addressed assets/, and a .gitcad product root
|
|
5
|
+
instancing everything at as-modeled positions). From there the whole
|
|
6
|
+
toolchain applies: viewer + explode, interference with clash budgets,
|
|
7
|
+
review gates, release.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> None: # pragma: no cover - CLI entrypoint
|
|
14
|
+
import argparse
|
|
15
|
+
|
|
16
|
+
from gitcad.importers.fcstd import fcstd_to_project
|
|
17
|
+
from gitcad.kernel import get_kernel
|
|
18
|
+
|
|
19
|
+
ap = argparse.ArgumentParser(
|
|
20
|
+
description="gitcad convert — foreign CAD file to a gitcad project")
|
|
21
|
+
ap.add_argument("file", help="an .FCStd file (more formats to come)")
|
|
22
|
+
ap.add_argument("-o", "--out", required=True, help="project directory")
|
|
23
|
+
ap.add_argument("--name", help="project name (default: file stem)")
|
|
24
|
+
args = ap.parse_args()
|
|
25
|
+
result = fcstd_to_project(args.file, args.out,
|
|
26
|
+
get_kernel(require="occt"), name=args.name)
|
|
27
|
+
print(f"converted {result['project']}: {len(result['bodies'])} bodies")
|
|
28
|
+
for rel in result["written"]:
|
|
29
|
+
print(f" {rel}")
|
|
30
|
+
print(f"open it: gitcad-view {args.out}/{result['root']}")
|
gitcad/explore.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""gitcad-explore — the branch scoreboard (ADR-0016's exploration half).
|
|
2
|
+
|
|
3
|
+
Spawn N design variants as branches, then judge them all by the same
|
|
4
|
+
gates: the review check-delta against base (violations introduced/fixed)
|
|
5
|
+
and, when the branch carries ``requirements.reqs``, the full executable
|
|
6
|
+
requirements suite run in a throwaway worktree of that branch. Output is
|
|
7
|
+
a table — the design-space comparison incumbent tools cannot make
|
|
8
|
+
because their files cannot branch.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import subprocess
|
|
14
|
+
import tempfile
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from gitcad.review import review_range
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _git(repo: str, *args: str) -> subprocess.CompletedProcess:
|
|
21
|
+
return subprocess.run(["git", "-C", repo, *args],
|
|
22
|
+
capture_output=True, text=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _branches(repo: str, base: str) -> list[str]:
|
|
26
|
+
proc = _git(repo, "branch", "--format=%(refname:short)")
|
|
27
|
+
return [b.strip() for b in proc.stdout.splitlines()
|
|
28
|
+
if b.strip() and b.strip() != base]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _requirements_in(repo: str, branch: str) -> str | None:
|
|
32
|
+
proc = _git(repo, "show", f"{branch}:requirements.reqs")
|
|
33
|
+
return proc.stdout if proc.returncode == 0 else None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def score_branches(repo: str, base: str,
|
|
37
|
+
branches: list[str] | None = None) -> dict:
|
|
38
|
+
from gitcad.requirements import verify
|
|
39
|
+
|
|
40
|
+
rows = []
|
|
41
|
+
for branch in (branches or _branches(repo, base)):
|
|
42
|
+
row: dict = {"branch": branch}
|
|
43
|
+
review = review_range(repo, base, branch)
|
|
44
|
+
row["gate"] = "PASS" if review["gate_ok"] else "FAIL"
|
|
45
|
+
row["introduced"] = review["summary"]["introduced"]
|
|
46
|
+
row["fixed"] = review["summary"]["fixed"]
|
|
47
|
+
row["files_changed"] = review["summary"]["changed"]
|
|
48
|
+
|
|
49
|
+
reqs_text = _requirements_in(repo, branch)
|
|
50
|
+
if reqs_text is not None:
|
|
51
|
+
with tempfile.TemporaryDirectory() as td:
|
|
52
|
+
wt = str(Path(td) / "wt")
|
|
53
|
+
added = _git(repo, "worktree", "add", "--detach", wt, branch)
|
|
54
|
+
if added.returncode == 0:
|
|
55
|
+
try:
|
|
56
|
+
report = verify(reqs_text, wt)
|
|
57
|
+
row["requirements"] = (
|
|
58
|
+
f"{report['summary']['pass']}/"
|
|
59
|
+
f"{len(report['requirements'])} pass")
|
|
60
|
+
row["requirements_ok"] = report["ok"]
|
|
61
|
+
finally:
|
|
62
|
+
_git(repo, "worktree", "remove", "--force", wt)
|
|
63
|
+
else:
|
|
64
|
+
row["requirements"] = "worktree failed"
|
|
65
|
+
row["requirements_ok"] = False
|
|
66
|
+
else:
|
|
67
|
+
row["requirements"] = "—"
|
|
68
|
+
row["requirements_ok"] = None
|
|
69
|
+
rows.append(row)
|
|
70
|
+
|
|
71
|
+
# rank: gate pass first, then requirements ok, then most fixed, fewest introduced
|
|
72
|
+
rows.sort(key=lambda r: (r["gate"] != "PASS",
|
|
73
|
+
r["requirements_ok"] is False,
|
|
74
|
+
-r["fixed"], r["introduced"], r["branch"]))
|
|
75
|
+
return {"base": base, "rows": rows}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def to_markdown(scoreboard: dict) -> str:
|
|
79
|
+
lines = [f"## branch scoreboard vs `{scoreboard['base']}`", "",
|
|
80
|
+
"| branch | gate | introduced | fixed | files | requirements |",
|
|
81
|
+
"|--------|------|-----------:|------:|------:|--------------|"]
|
|
82
|
+
for r in scoreboard["rows"]:
|
|
83
|
+
lines.append(f"| {r['branch']} | **{r['gate']}** | {r['introduced']} "
|
|
84
|
+
f"| {r['fixed']} | {r['files_changed']} | {r['requirements']} |")
|
|
85
|
+
return "\n".join(lines) + "\n"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main() -> None: # pragma: no cover - CLI entrypoint
|
|
89
|
+
import argparse
|
|
90
|
+
|
|
91
|
+
ap = argparse.ArgumentParser(
|
|
92
|
+
description="gitcad explore — judge N design branches by the same gates")
|
|
93
|
+
ap.add_argument("branches", nargs="*",
|
|
94
|
+
help="branches to score (default: all except base)")
|
|
95
|
+
ap.add_argument("--repo", default=".")
|
|
96
|
+
ap.add_argument("--base", required=True)
|
|
97
|
+
args = ap.parse_args()
|
|
98
|
+
print(to_markdown(score_branches(args.repo, args.base,
|
|
99
|
+
args.branches or None)))
|