gitcad 0.7.7__tar.gz
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-0.7.7/.gitignore +38 -0
- gitcad-0.7.7/PKG-INFO +70 -0
- gitcad-0.7.7/README.md +42 -0
- gitcad-0.7.7/pyproject.toml +57 -0
- gitcad-0.7.7/src/gitcad/bench/__init__.py +0 -0
- gitcad-0.7.7/src/gitcad/bench/corpus.py +260 -0
- gitcad-0.7.7/src/gitcad/bench/scorecard.py +176 -0
- gitcad-0.7.7/src/gitcad/bridge.py +72 -0
- gitcad-0.7.7/src/gitcad/convert.py +30 -0
- gitcad-0.7.7/src/gitcad/explore.py +99 -0
- gitcad-0.7.7/src/gitcad/fasteners.py +112 -0
- gitcad-0.7.7/src/gitcad/init.py +134 -0
- gitcad-0.7.7/src/gitcad/lots.py +136 -0
- gitcad-0.7.7/src/gitcad/mcp/__init__.py +15 -0
- gitcad-0.7.7/src/gitcad/mcp/server.py +1065 -0
- gitcad-0.7.7/src/gitcad/merge3.py +289 -0
- gitcad-0.7.7/src/gitcad/pcba.py +114 -0
- gitcad-0.7.7/src/gitcad/release.py +223 -0
- gitcad-0.7.7/src/gitcad/render.py +177 -0
- gitcad-0.7.7/src/gitcad/requirements.py +204 -0
- gitcad-0.7.7/src/gitcad/review.py +230 -0
- gitcad-0.7.7/src/gitcad/viewer/__init__.py +19 -0
- gitcad-0.7.7/src/gitcad/viewer/boardsvg.py +80 -0
- gitcad-0.7.7/src/gitcad/viewer/page.py +577 -0
- gitcad-0.7.7/src/gitcad/viewer/server.py +433 -0
gitcad-0.7.7/.gitignore
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Generated geometry is a build artifact, NOT source (ADR-0004). Never commit it.
|
|
2
|
+
*.brep
|
|
3
|
+
*.step
|
|
4
|
+
*.stp
|
|
5
|
+
*.stl
|
|
6
|
+
*.gltf
|
|
7
|
+
*.glb
|
|
8
|
+
*.iges
|
|
9
|
+
*.igs
|
|
10
|
+
# Drawings are generated from the model too.
|
|
11
|
+
/build-artifacts/
|
|
12
|
+
*.dxf
|
|
13
|
+
# PDFs generated from drawings (keep hand-authored docs elsewhere if needed).
|
|
14
|
+
/out/
|
|
15
|
+
|
|
16
|
+
# Python
|
|
17
|
+
__pycache__/
|
|
18
|
+
*.py[cod]
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.eggs/
|
|
21
|
+
build/
|
|
22
|
+
dist/
|
|
23
|
+
.venv/
|
|
24
|
+
venv/
|
|
25
|
+
|
|
26
|
+
# Tooling
|
|
27
|
+
.pytest_cache/
|
|
28
|
+
.coverage
|
|
29
|
+
.coverage.*
|
|
30
|
+
htmlcov/
|
|
31
|
+
.mypy_cache/
|
|
32
|
+
.ruff_cache/
|
|
33
|
+
|
|
34
|
+
# Editors / OS
|
|
35
|
+
.vscode/
|
|
36
|
+
.idea/
|
|
37
|
+
.DS_Store
|
|
38
|
+
Thumbs.db
|
gitcad-0.7.7/PKG-INFO
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gitcad
|
|
3
|
+
Version: 0.7.7
|
|
4
|
+
Summary: Agent-first, headless, git-native CAD (metapackage: core + mech + ecad + MCP + viewer + release)
|
|
5
|
+
Project-URL: Homepage, https://gitcad.xyz
|
|
6
|
+
Project-URL: Repository, https://github.com/gitcad-xyz/gitcad
|
|
7
|
+
Project-URL: Issues, https://github.com/gitcad-xyz/gitcad/issues
|
|
8
|
+
Author-email: Dan Willis <danielcwillis@gmail.com>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
Keywords: agent,brep,cad,eda,git,headless,mcp,occt
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Manufacturing
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: gitcad-core==0.7.7
|
|
22
|
+
Requires-Dist: gitcad-ecad==0.7.7
|
|
23
|
+
Requires-Dist: gitcad-mech==0.7.7
|
|
24
|
+
Requires-Dist: mcp>=1.0
|
|
25
|
+
Provides-Extra: occt
|
|
26
|
+
Requires-Dist: gitcad-mech[occt]==0.7.7; extra == 'occt'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# gitcad
|
|
30
|
+
|
|
31
|
+
**Agent-first, headless, git-native CAD — mechanical and electrical, one substrate.**
|
|
32
|
+
|
|
33
|
+
gitcad is a boundary-representation CAD system designed to be driven by agents and
|
|
34
|
+
version-controlled like source. Models are byte-canonical text (geometry is a
|
|
35
|
+
build artifact, never committed); identity is construction-lineage based, never
|
|
36
|
+
ordinal; every build produces a reviewable, diffable report.
|
|
37
|
+
|
|
38
|
+
This metapackage installs the whole system:
|
|
39
|
+
|
|
40
|
+
| package | domain |
|
|
41
|
+
|---|---|
|
|
42
|
+
| [`gitcad-core`](https://pypi.org/project/gitcad-core/) | canonical text, stable identity, the Part standard, report pipeline |
|
|
43
|
+
| [`gitcad-mech`](https://pypi.org/project/gitcad-mech/) | document model, **exact B-rep kernel**, sketches, drawings, importers |
|
|
44
|
+
| [`gitcad-ecad`](https://pypi.org/project/gitcad-ecad/) | schematic + ERC, board + DRC, fab outputs, KiCad import |
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install gitcad # full system: both domains, exact+native kernel, MCP server
|
|
50
|
+
pip install gitcad[occt] # + OCCT kernel oracle/fallback (cadquery-ocp)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`pip install gitcad` is complete and agent-ready out of the box — mechanical and
|
|
54
|
+
electrical, the exact kernel with its native accelerator, and the `gitcad-mcp`
|
|
55
|
+
Model Context Protocol server. Only OCCT (a heavy optional oracle) is an extra.
|
|
56
|
+
|
|
57
|
+
The mechanical kernel is **exact**: topological decisions are made in rational
|
|
58
|
+
arithmetic (ℚ), never by a floating-point tolerance — see
|
|
59
|
+
[`forgekernel`](https://pypi.org/project/forgekernel/) and the
|
|
60
|
+
[benchmarks vs OCCT](https://github.com/gitcad-xyz/gitcad/blob/main/bench/RUST-vs-OCCT.md).
|
|
61
|
+
The compiled Rust build of the hot paths installs **by default** on mainstream
|
|
62
|
+
platforms (identical results, only faster); on an architecture with no wheel it is
|
|
63
|
+
skipped and the kernel runs pure-Python — same answers, nothing breaks.
|
|
64
|
+
|
|
65
|
+
## Console tools
|
|
66
|
+
|
|
67
|
+
`gitcad-init`, `gitcad-render`, `gitcad-view`, `gitcad-review`, `gitcad-merge`,
|
|
68
|
+
`gitcad-verify`, `gitcad-convert`, `gitcad-explore`, `gitcad-mcp`, `gitcad-lot`.
|
|
69
|
+
|
|
70
|
+
Apache-2.0 · https://gitcad.xyz · https://github.com/gitcad-xyz/gitcad
|
gitcad-0.7.7/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# gitcad
|
|
2
|
+
|
|
3
|
+
**Agent-first, headless, git-native CAD — mechanical and electrical, one substrate.**
|
|
4
|
+
|
|
5
|
+
gitcad is a boundary-representation CAD system designed to be driven by agents and
|
|
6
|
+
version-controlled like source. Models are byte-canonical text (geometry is a
|
|
7
|
+
build artifact, never committed); identity is construction-lineage based, never
|
|
8
|
+
ordinal; every build produces a reviewable, diffable report.
|
|
9
|
+
|
|
10
|
+
This metapackage installs the whole system:
|
|
11
|
+
|
|
12
|
+
| package | domain |
|
|
13
|
+
|---|---|
|
|
14
|
+
| [`gitcad-core`](https://pypi.org/project/gitcad-core/) | canonical text, stable identity, the Part standard, report pipeline |
|
|
15
|
+
| [`gitcad-mech`](https://pypi.org/project/gitcad-mech/) | document model, **exact B-rep kernel**, sketches, drawings, importers |
|
|
16
|
+
| [`gitcad-ecad`](https://pypi.org/project/gitcad-ecad/) | schematic + ERC, board + DRC, fab outputs, KiCad import |
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install gitcad # full system: both domains, exact+native kernel, MCP server
|
|
22
|
+
pip install gitcad[occt] # + OCCT kernel oracle/fallback (cadquery-ocp)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`pip install gitcad` is complete and agent-ready out of the box — mechanical and
|
|
26
|
+
electrical, the exact kernel with its native accelerator, and the `gitcad-mcp`
|
|
27
|
+
Model Context Protocol server. Only OCCT (a heavy optional oracle) is an extra.
|
|
28
|
+
|
|
29
|
+
The mechanical kernel is **exact**: topological decisions are made in rational
|
|
30
|
+
arithmetic (ℚ), never by a floating-point tolerance — see
|
|
31
|
+
[`forgekernel`](https://pypi.org/project/forgekernel/) and the
|
|
32
|
+
[benchmarks vs OCCT](https://github.com/gitcad-xyz/gitcad/blob/main/bench/RUST-vs-OCCT.md).
|
|
33
|
+
The compiled Rust build of the hot paths installs **by default** on mainstream
|
|
34
|
+
platforms (identical results, only faster); on an architecture with no wheel it is
|
|
35
|
+
skipped and the kernel runs pure-Python — same answers, nothing breaks.
|
|
36
|
+
|
|
37
|
+
## Console tools
|
|
38
|
+
|
|
39
|
+
`gitcad-init`, `gitcad-render`, `gitcad-view`, `gitcad-review`, `gitcad-merge`,
|
|
40
|
+
`gitcad-verify`, `gitcad-convert`, `gitcad-explore`, `gitcad-mcp`, `gitcad-lot`.
|
|
41
|
+
|
|
42
|
+
Apache-2.0 · https://gitcad.xyz · https://github.com/gitcad-xyz/gitcad
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gitcad"
|
|
7
|
+
version = "0.7.7"
|
|
8
|
+
description = "Agent-first, headless, git-native CAD (metapackage: core + mech + ecad + MCP + viewer + release)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
authors = [{name = "Dan Willis", email = "danielcwillis@gmail.com"}]
|
|
13
|
+
keywords = ["cad", "brep", "occt", "mcp", "agent", "headless", "git", "eda"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Intended Audience :: Manufacturing",
|
|
18
|
+
"License :: OSI Approved :: Apache Software License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: Scientific/Engineering",
|
|
22
|
+
"Topic :: Multimedia :: Graphics :: 3D Modeling",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)",
|
|
24
|
+
]
|
|
25
|
+
# Agent-first: the MCP server ships by default (gitcad-mcp entry point). The
|
|
26
|
+
# compiled kernel (forgekernel_rs) is likewise a default dep of gitcad-mech on
|
|
27
|
+
# mainstream architectures — no extra needed for either.
|
|
28
|
+
dependencies = [
|
|
29
|
+
"gitcad-core==0.7.7",
|
|
30
|
+
"gitcad-mech==0.7.7",
|
|
31
|
+
"gitcad-ecad==0.7.7",
|
|
32
|
+
"mcp>=1.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
# OCCT oracle/fallback kernel.
|
|
37
|
+
occt = ["gitcad-mech[occt]==0.7.7"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://gitcad.xyz"
|
|
41
|
+
Repository = "https://github.com/gitcad-xyz/gitcad"
|
|
42
|
+
Issues = "https://github.com/gitcad-xyz/gitcad/issues"
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
gitcad-mcp = "gitcad.mcp.server:main"
|
|
46
|
+
gitcad-view = "gitcad.viewer.server:main"
|
|
47
|
+
gitcad-review = "gitcad.review:main"
|
|
48
|
+
gitcad-merge = "gitcad.merge3:main"
|
|
49
|
+
gitcad-verify = "gitcad.requirements:main"
|
|
50
|
+
gitcad-lot = "gitcad.lots:main"
|
|
51
|
+
gitcad-init = "gitcad.init:main"
|
|
52
|
+
gitcad-render = "gitcad.render:main"
|
|
53
|
+
gitcad-convert = "gitcad.convert:main"
|
|
54
|
+
gitcad-explore = "gitcad.explore:main"
|
|
55
|
+
|
|
56
|
+
[tool.hatch.build.targets.wheel]
|
|
57
|
+
packages = ["src/gitcad"]
|
|
File without changes
|
|
@@ -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()
|