bench: matrix v5 adds mech_assembly; the correctness suite grows with the mechanical and sheet checks
The evidence for tracks B and G, in the shape tracks A and E used.
- benchmarks/tasks_v5.py: 18 tasks, DEFAULT_MATRIX = "v5"; v4, v3 and v2 stay
addressable so an earlier report reproduces exactly rather than being only
described. mech_assembly is roadmap criterion 3 - a flange-coupling sheet
built from the part model and the sheet standard, through the code the
tools call (apply_titleblock, draw_part/add_view/dimension_part,
insert_std_part, extract_records -> draw_bom_table, add_balloon), gated on
two numbers that cannot be negotiated: drawing_critique(focus=None) returns
zero issues and the finalize score is at least 90 (it scores 100, 18/18 on
the v5 matrix). The adapter returns the focuses that fired, so a
regression names itself, and a test drops one balloon to prove the gate
fails and names mech_bom_balloon_mismatch.
- Seven correctness checks (32 -> 39). mech_section_hatch_area is the one
that cannot be faked: a sleeve of length 60, outside diameter 40 and bore
20 cuts two faces totalling 1200 mm^2 exactly, read back out of the drawing
through entity_measure. sheet_frame_iso5457_a3 pins the A3 frame (20,10 to
410,287 inside the 420 x 297 trimmed sheet); mech_iso286_on_dimension pins
40 H7 at +0.025/0 through the layout; std_part_iso4014_m12 pins s = 18 and
k = 7.5 by value; mech_thread_unrepresented_is_caught is the seventh: a
quality gate that never fires is not a gate.
- scripts/smoke_mech_com.py executed every new COM path once on AutoCAD 2026
(25.1s): frame, title block, a sectioned shaft with 12 dimensions, an ISO
4014 block with 1 ACADMCP_MECH chunk, a weld symbol, the parts list read off
the drawing, a linked balloon, an xref attached / listed / detached, a
31687-byte .dwg (AC1032) through ActiveX SaveAs, critique 0 issues, exit 0,
every scratch document closed. It retries RPC_E_CALL_REJECTED (measured:
the first call after Documents.Add is refused) and tracks its documents by
the document list, since SaveAs renames them.
- Defects the evidence found, each fixed with a test that fails on the old
code:
* ezdxf dimension_diameter put the text beyond the SECOND chord point;
ActiveX AddDimDiametric puts it beyond the first (measured live: a d50
from (x,170) to (x,220), leader 10, text at (x,160)). Headlessly every
front-view diameter stood on its own chain dimension. ezdxf now follows
ActiveX.
* draw_part's anchor goes on MECH; ActiveX refuses entity.Layer on a
missing layer (measured -0x7ffdfff7 'Key not found'). draw.py ensures
every target layer before the first entity.
* bom.extract_records filtered on INSERT; ActiveX names it BLOCKREFERENCE
(ObjectName AcDbBlockReference) - the live parts list was empty.
* COM _entity_info reported no dimension text_position: ModelSpace.Item
narrows to IAcadEntity, which has no TextPosition (measured
AttributeError); read through _com_unnarrow (TextPosition measured on
AcDbDiametricDimension, WCS). dim_overlap compared bbox centres and a
diameter collided with its own bore.
* pid graph raised KeyError 'start' mid-critique on a LINE whose point read
AutoCAD refused while busy; such a line is left out.
* the validator read "ISO 4014 - M12x60" as fake dimension text.
- The published A/B report is re-run over the grown suite rather than
re-typed (26/39 -> 39/39, 13 fixed, 0 regressed), check_doc_numbers derives
the task count from the v5 matrix, the token table is remeasured with a
TOOL_PACKS=core,mech row (193 tools / 61,673 tokens), and the README's
collected-tests figure is retaken (3990).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3b033033f9
commit
898e775f93
+40
-9
@@ -41,13 +41,13 @@ Unsupported tasks remain in the fixed-matrix denominator with score zero,
|
||||
preventing partial implementations from receiving an inflated score.
|
||||
|
||||
```bash
|
||||
python -m benchmarks.run_competitors --list --matrix v4
|
||||
python -m benchmarks.run_competitors --server autocad-mcp-pro --backend ezdxf --matrix v4 --json
|
||||
python -m benchmarks.run_competitors --list --matrix v5
|
||||
python -m benchmarks.run_competitors --server autocad-mcp-pro --backend ezdxf --matrix v5 --json
|
||||
python -m benchmarks.run_competitors --task table_mleader --task hatch_islands --json
|
||||
```
|
||||
|
||||
The release-machine ezdxf self-check is **17/17 (100.0)** on the v4 matrix
|
||||
(`--matrix v3` / `--matrix v2` reproduce the earlier sets).
|
||||
The release-machine ezdxf self-check is **18/18 (100.0)** on the v5 matrix
|
||||
(`--matrix v4` / `--matrix v3` / `--matrix v2` reproduce the earlier sets).
|
||||
Repository stars and raw tool counts do not contribute to the score. Adapter
|
||||
registration lives in `competitors.yaml`.
|
||||
|
||||
@@ -100,6 +100,23 @@ fail on their own:
|
||||
The competitor reports carry no result for either, for the same reason they
|
||||
carry none for the v3 five. `--matrix v3` reproduces the v1.5 set exactly.
|
||||
|
||||
### Matrix v5 (v1.6) — the mechanical sheet
|
||||
|
||||
`tasks_v5.py` keeps the v4 seventeen unchanged and adds roadmap criterion 3:
|
||||
|
||||
| Task | Category | Verified against |
|
||||
|---|---|---|
|
||||
| `mech_assembly` | mech | a flange-coupling A3 sheet built through the code the tools call — `apply_titleblock` (ISO 5457 frame with zones and marks, ISO 7200 title block), a hub drawn by `draw_part` with a full section by `add_view` and chain dimensions by `dimension_part`, a flange with its bolt-circle end view, two ISO 4014 M12x60 bolts and two ISO 4032 M12 nuts by `insert_std_part`, the ISO 7573 parts list *read off the drawing* by `extract_records` and drawn by `draw_bom_table`, and one balloon per row linked to that row's inserts by `add_balloon`. The sheet is saved, then gated on two numbers that cannot be negotiated: `run_critique(focus=None)` returns **zero** issues and `combine(validator, critique)` scores at least **90**. The focuses that fired come back in the metrics, and `tests/test_benchmark_v5.py` drops one balloon to prove the gate fails and names `mech_bom_balloon_mismatch` |
|
||||
|
||||
Nothing in the task tidies up after the tools: the view engine draws the ISO
|
||||
128-23 centre lines of every circle it emits, so the sheet is clean because it
|
||||
was drawn clean. Building it found four defects that every headless test had
|
||||
missed — the headless diameter text on the wrong side of the chord, the
|
||||
validator reading a fastener designation (`M12x60`) as a fake dimension, and on
|
||||
the live seat a missing `MECH` layer and a parts list that could not see a
|
||||
`BLOCKREFERENCE`; each is fixed with a test. The competitor reports carry no
|
||||
result for it.
|
||||
|
||||
## Live competitor lane (v1.4)
|
||||
|
||||
Two competitor adapters now execute the exact same task matrix, black-box over
|
||||
@@ -217,15 +234,29 @@ python benchmarks/compare_versions.py --json results.json
|
||||
|
||||
### Result — this branch (1.6.0-dev) vs v1.5.1 (release gate)
|
||||
|
||||
32 checks, ezdxf backend, one subprocess per check. Machine-readable report:
|
||||
39 checks, ezdxf backend, one subprocess per check. Machine-readable report:
|
||||
[`results/published/ab-v1.5.1-vs-v1.6.0-dev.json`](results/published/ab-v1.5.1-vs-v1.6.0-dev.json).
|
||||
|
||||
| Version | Checks passing | Pass rate | Fixed | Regressed |
|
||||
|---------|----------------|-----------|-------|-----------|
|
||||
| **v1.5.1** (baseline) | 26 / 32 | 81.2 % | — | — |
|
||||
| **v1.6.0-dev** (this branch) | 32 / 32 | 100 % | 6 | **0** |
|
||||
| **v1.5.1** (baseline) | 26 / 39 | 66.7 % | — | — |
|
||||
| **v1.6.0-dev** (this branch) | 39 / 39 | 100 % | 13 | **0** |
|
||||
|
||||
The six are all `miss → pass` — v1.5.1 has none of the methods: the three
|
||||
The thirteen are all `miss → pass` — v1.5.1 has none of the methods. Seven
|
||||
are tracks B and G: `mech_part_roundtrip` (a part read back from its own
|
||||
`ACADMCP_MECH` XDATA equals the part drawn), `mech_section_hatch_area` (a
|
||||
sleeve 60 long, ⌀40 outside, ⌀20 bore: a full section cuts two 60 × 10 faces,
|
||||
1200 mm² exactly, measured out of the drawing by `entity_measure` — never from
|
||||
the code that drew it), `mech_iso286_on_dimension` (ISO 286-1: 40 H7 is
|
||||
+0.025 / 0, and the fit survives `layout_dimensions`),
|
||||
`mech_thread_unrepresented_is_caught` (the ISO 6410 focus fires on a thread
|
||||
drawn as a plain circle and stops firing once the thin ¾ arc is there — the
|
||||
mechanical twin of `dim_overlap_critique_fires`), `std_part_iso4014_m12` (ISO
|
||||
4014:2011 table 1, M12: s = 18, k = 7.5, matched by value; the inserted block
|
||||
carries its designation back), `sheet_frame_iso5457_a3` (the frame runs 20,10
|
||||
to 410,287 inside the 420 × 297 trimmed sheet) and `bom_balloon_link` (two
|
||||
identical bolts collapse into one row of quantity 2 with a stable item number,
|
||||
and that row's balloon carries it). The other six are the three
|
||||
P&ID checks track A added (`pid_block_define_attdef_roundtrip`,
|
||||
`pid_tag_parse_fic`, `pid_graph_edge_count`) and the three settings checks of
|
||||
track E (`settings_dimstyle_iso25_values`: the ISO-25 preset reaches the
|
||||
@@ -234,7 +265,7 @@ through the lister; `settings_layer_state_roundtrip`: save → change → restor
|
||||
puts the layer table back and the state survives save/reopen as an XRECORD in
|
||||
the file; `settings_pdf_mediabox_a3`: the plotted PDF's own `/MediaBox` reads
|
||||
420 × 297 mm). Every one of the 26 checks it was released on still passes, so
|
||||
the two tracks added capability without moving a number it had already earned.
|
||||
the four tracks added capability without moving a number it had already earned.
|
||||
|
||||
### Result — v1.5.1 vs v1.5.0
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Reference adapter for this repository, covering the v4 task matrix.
|
||||
"""Reference adapter for this repository, covering the v5 task matrix.
|
||||
|
||||
Every task verifies against a value worked out independently of the code under
|
||||
test — a closed-form area, a count of entities placed on purpose, a token
|
||||
@@ -12,7 +12,7 @@ import math
|
||||
from pathlib import Path
|
||||
|
||||
from benchmarks.adapters.base import BenchmarkAdapter, TaskResult
|
||||
from benchmarks.tasks_v4 import TaskSpec
|
||||
from benchmarks.tasks_v5 import TaskSpec
|
||||
from engineering.delivery import deliver_drawing
|
||||
from engineering.layers import ensure_engineering_layers, ensure_standard_linetypes
|
||||
from engineering.refiner import refine_drawing
|
||||
@@ -514,6 +514,174 @@ class AutoCADMCPProAdapter(BenchmarkAdapter):
|
||||
[str(row_before["path"]), str(row["path"])],
|
||||
)
|
||||
|
||||
async def _task_mech_assembly(self):
|
||||
"""A flange-coupling sheet built from the part model and the sheet standard.
|
||||
|
||||
Roadmap criterion 3. Two parts (a hub with a bore and a flange), a full
|
||||
section through the hub, four standard fasteners with their parts-list
|
||||
rows and balloons, an ISO 5457 A3 frame and an ISO 7200 title block.
|
||||
Every step goes through the code the tools call: the frame and title
|
||||
block through ``apply_titleblock``, the parts list is *read* off the
|
||||
drawing by ``extract_records`` and drawn by ``draw_bom_table``, and each
|
||||
balloon is linked to its row's inserts by ``add_balloon``.
|
||||
|
||||
The gate is two numbers, and both are asserted rather than reported:
|
||||
``run_critique(focus=None)`` must return zero issues, and the score the
|
||||
finalize gate computes - ``combine(validator.summary, critique)`` over
|
||||
the saved sheet - must be at least 90. A regression in either fails the
|
||||
task; the focuses that fired come back in the metrics so the failure
|
||||
names itself.
|
||||
|
||||
No step here repairs what the critique would flag: the view engine
|
||||
draws the ISO 128-23 centre lines of every circle it emits (the end
|
||||
view's bore and bolt holes included), so the sheet is clean because
|
||||
the tools drew it clean, not because the benchmark tidied up after them.
|
||||
"""
|
||||
from engineering import DrawingValidator
|
||||
from engineering.critique import run_critique
|
||||
from engineering.layers import apply_layer_set
|
||||
from engineering.mech.draw import add_view, dimension_part, draw_part
|
||||
from engineering.mech.part import build_part
|
||||
from engineering.mech.stdparts import insert_std_part
|
||||
from engineering.scoring import combine
|
||||
from engineering.sheet.bom import (
|
||||
add_balloon,
|
||||
draw_bom_table,
|
||||
extract_records,
|
||||
rows_from_records,
|
||||
)
|
||||
from engineering.sheet.titleblock import (
|
||||
TB_HEIGHT,
|
||||
TitleBlockMetadata,
|
||||
apply_titleblock,
|
||||
titleblock_origin,
|
||||
)
|
||||
|
||||
await apply_layer_set(self.backend, "mech")
|
||||
await apply_titleblock(
|
||||
self.backend,
|
||||
size="A3",
|
||||
metadata=TitleBlockMetadata(
|
||||
title="FLANGE COUPLING",
|
||||
drawing_no="MA-001",
|
||||
drawn_by="autocad-mcp-pro",
|
||||
scale="1:1",
|
||||
material="S235JR",
|
||||
),
|
||||
projection="first",
|
||||
frame=True,
|
||||
zones=True,
|
||||
marks=True,
|
||||
)
|
||||
|
||||
hub = build_part(
|
||||
{
|
||||
"kind": "revolved",
|
||||
"name": "HUB",
|
||||
"material": "steel",
|
||||
"segments": [
|
||||
{"length": 40.0, "d_outer": 40.0, "d_inner": 20.0},
|
||||
{"length": 15.0, "d_outer": 90.0, "d_inner": 20.0},
|
||||
],
|
||||
}
|
||||
)
|
||||
flange = build_part(
|
||||
{
|
||||
"kind": "revolved",
|
||||
"name": "FLANGE",
|
||||
"material": "cast_iron",
|
||||
"segments": [{"length": 15.0, "d_outer": 90.0, "d_inner": 40.0}],
|
||||
# Four M12 clearance holes (ISO 273 medium, 13.5) on a 65 PCD.
|
||||
"features": [
|
||||
{
|
||||
"kind": "hole_circle",
|
||||
"id": "bolts",
|
||||
"x": 7.5,
|
||||
"pcd": 65.0,
|
||||
"count": 4,
|
||||
"diameter": 13.5,
|
||||
"start_angle": 45.0,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
# A3 landscape: the frame runs (20, 10)-(410, 287) and the title block
|
||||
# fills its lower-right corner, 180 x 60. First angle throughout: the
|
||||
# hub's front view sits at x = 125 so its section lands left of it
|
||||
# inside the frame; the flange's front view stands at x = 345 so its
|
||||
# end view (the bolt circle) lands between the two parts; the
|
||||
# fasteners take the free band below, left of the title block.
|
||||
drawn_hub = await draw_part(
|
||||
self.backend, hub, at=(125.0, 160.0), views=("front",), dimension=False
|
||||
)
|
||||
# A full longitudinal section along the hub's axis (part-local frame).
|
||||
await add_view(
|
||||
self.backend,
|
||||
drawn_hub["part_id"],
|
||||
"section",
|
||||
plane={"p1": [0.0, 0.0], "p2": [55.0, 0.0], "label": "A"},
|
||||
style="full",
|
||||
)
|
||||
await dimension_part(self.backend, drawn_hub["part_id"], style="chain")
|
||||
await draw_part(
|
||||
self.backend, flange, at=(345.0, 160.0), views=("front", "side"), dimension=True
|
||||
)
|
||||
|
||||
# Two bolt-and-nut pairs: each nut sits on the thread of its bolt.
|
||||
stations = (60.0, 110.0, 160.0, 210.0)
|
||||
fasteners = []
|
||||
for index, x in enumerate(stations):
|
||||
designation = "ISO 4014 - M12x60" if index % 2 == 0 else "ISO 4032 - M12"
|
||||
fasteners.append(
|
||||
await insert_std_part(self.backend, designation, at=(x, 90.0), view="side")
|
||||
)
|
||||
|
||||
# The parts list is a read of the drawing, not a list typed beside it.
|
||||
records = await extract_records(self.backend)
|
||||
rows = rows_from_records(records)
|
||||
tb_x, tb_y = titleblock_origin("A3")
|
||||
await draw_bom_table(self.backend, rows, at=(tb_x, tb_y + TB_HEIGHT))
|
||||
station_of = {
|
||||
str(result.get("handle")): x for result, x in zip(fasteners, stations, strict=True)
|
||||
}
|
||||
for row in rows:
|
||||
targets = [str(handle) for handle in row.get("handles") or ()]
|
||||
x = station_of.get(targets[0], stations[0]) if targets else stations[0]
|
||||
await add_balloon(
|
||||
self.backend,
|
||||
item=int(row["item"]),
|
||||
at=(x, 135.0),
|
||||
leader_to=(x, 95.0),
|
||||
targets=targets,
|
||||
)
|
||||
|
||||
# drawing_finalize saves before it validates; so does the benchmark,
|
||||
# or the validator's not_saved/no_path findings would grade the
|
||||
# harness instead of the sheet.
|
||||
sheet = self.artifact_dir / "mech_assembly.dxf"
|
||||
await self.backend.drawing_save_as(str(sheet))
|
||||
issues = await run_critique(self.backend, None)
|
||||
validation = await DrawingValidator().run(self.backend)
|
||||
summary = {"error": 0, "warning": 0, "info": 0}
|
||||
for issue in issues:
|
||||
summary[issue.severity] = summary.get(issue.severity, 0) + 1
|
||||
score = combine(validation.summary, summary)
|
||||
passed = len(issues) == 0 and float(score["score"]) >= 90.0
|
||||
return (
|
||||
passed,
|
||||
{
|
||||
"critique_issues": len(issues),
|
||||
"critique_focuses": sorted({issue.focus for issue in issues}),
|
||||
"score": float(score["score"]),
|
||||
"grade": score["grade"],
|
||||
"invalidity_ratio": score["invalidity_ratio"],
|
||||
"parts_list_rows": len(rows),
|
||||
"fasteners": len(fasteners),
|
||||
"validator_findings": sorted(f.code for f in validation.findings),
|
||||
},
|
||||
[str(sheet)],
|
||||
)
|
||||
|
||||
async def _task_auditable_delivery(self):
|
||||
await ensure_engineering_layers(self.backend)
|
||||
await self.backend.entity_create_line(0, 0, 10, 0, layer="GEOMETRY")
|
||||
|
||||
@@ -417,6 +417,231 @@ async def settings_pdf_mediabox_a3():
|
||||
)
|
||||
|
||||
|
||||
# ── Mechanical parts and the sheet (tracks B + G) ───────────────────────────
|
||||
|
||||
|
||||
def _field(row: dict, *names):
|
||||
"""A row's value under whichever of these names it carries.
|
||||
|
||||
The parts-list column spelling belongs to the sheet module; what this
|
||||
suite pins is the numbers, so it reads by meaning rather than by name.
|
||||
"""
|
||||
for name in names:
|
||||
if name in row:
|
||||
return row[name]
|
||||
return None
|
||||
|
||||
|
||||
def _inserted_handle(result: dict):
|
||||
return result.get("handle") or (result.get("handles") or [None])[0]
|
||||
|
||||
|
||||
async def mech_part_roundtrip():
|
||||
"""Draw a part, read it back from its own ACADMCP_MECH XDATA, compare.
|
||||
|
||||
Segments and material only: a feature's parameter spelling belongs to
|
||||
`engineering/mech/features.py` and its own tests, and a check that guessed
|
||||
at it would fail for the wrong reason.
|
||||
"""
|
||||
from engineering.mech.draw import draw_part, read_part
|
||||
from engineering.mech.part import build_part, part_to_dict
|
||||
|
||||
b = await _b()
|
||||
spec = {
|
||||
"kind": "revolved",
|
||||
"name": "SHAFT",
|
||||
"material": "steel",
|
||||
"segments": [
|
||||
{"length": 40.0, "d_outer": 30.0},
|
||||
{"length": 20.0, "d_outer": 50.0, "d_inner": 10.0},
|
||||
],
|
||||
}
|
||||
part = build_part(spec)
|
||||
drawn = await draw_part(b, part, at=(0.0, 0.0), views=("front",), dimension=False)
|
||||
back = await read_part(b, drawn["part_id"])
|
||||
return part_to_dict(build_part(back["part"])) == part_to_dict(part)
|
||||
|
||||
|
||||
async def mech_section_hatch_area():
|
||||
"""The hatched cut area of a full section, measured against a hand number.
|
||||
|
||||
A plain sleeve: length 60, outside diameter 40, bore 20. A full
|
||||
longitudinal section cuts two faces, each 60 x 10, so the hatch fills
|
||||
60 * (40 - 20) = 1200 mm^2 exactly. The number is read out of the drawing
|
||||
through entity_measure (the tool's analysis_measure_entity), never from the
|
||||
code that drew it — which is why it cannot be faked.
|
||||
"""
|
||||
from engineering.mech.draw import add_view, draw_part
|
||||
from engineering.mech.part import build_part
|
||||
|
||||
b = await _b()
|
||||
part = build_part(
|
||||
{
|
||||
"kind": "revolved",
|
||||
"name": "SLEEVE",
|
||||
"material": "steel",
|
||||
"segments": [{"length": 60.0, "d_outer": 40.0, "d_inner": 20.0}],
|
||||
}
|
||||
)
|
||||
drawn = await draw_part(b, part, at=(0.0, 0.0), views=("front",), dimension=False)
|
||||
await add_view(
|
||||
b,
|
||||
drawn["part_id"],
|
||||
"section",
|
||||
plane={"p1": [0.0, 0.0], "p2": [60.0, 0.0], "label": "A"},
|
||||
style="full",
|
||||
)
|
||||
total = 0.0
|
||||
for ent in await b.entity_list(type_filter="HATCH", limit=500):
|
||||
total += float((await b.entity_measure(ent.handle))["area"])
|
||||
return abs(total - 1200.0) < 0.01
|
||||
|
||||
|
||||
async def mech_iso286_on_dimension():
|
||||
"""A 40 H7 bore dimension carries ISO 286's +0.025 / 0 through the layout.
|
||||
|
||||
ISO 286-1 table 1: the 30-50 mm step has IT7 = 25 um, and H fixes the lower
|
||||
deviation at 0. The fit has to survive `layout_dimensions`, or the number
|
||||
is right in the table and wrong on the sheet.
|
||||
"""
|
||||
from engineering.fits import fit_lookup
|
||||
from engineering.mech.dimension import layout_dimensions
|
||||
from engineering.mech.primitives import Circle, DimIntent
|
||||
from engineering.mech.views import View
|
||||
|
||||
deviation = fit_lookup("H7", 40.0)
|
||||
if abs(deviation.upper_mm - 0.025) > 1e-9 or abs(deviation.lower_mm) > 1e-9:
|
||||
return False
|
||||
intent = DimIntent("diameter", (-20.0, 0.0), (20.0, 0.0), fit="H7", feature="bore")
|
||||
view = View(
|
||||
kind="front",
|
||||
prims=(Circle((0.0, 0.0), 20.0),),
|
||||
dims=(intent,),
|
||||
omitted=(),
|
||||
bbox=(-20.0, -20.0, 20.0, 20.0),
|
||||
)
|
||||
placed = layout_dimensions(view, [intent])
|
||||
return len(placed) == 1 and placed[0].fit == "H7" and placed[0].feature == "bore"
|
||||
|
||||
|
||||
async def mech_thread_unrepresented_is_caught():
|
||||
"""The ISO 6410 focus fires on a thread drawn as a plain circle.
|
||||
|
||||
A quality gate that never fires is not a gate; this is the mechanical twin
|
||||
of `dim_overlap_critique_fires`.
|
||||
"""
|
||||
from engineering.mech.critique import build_index, issues_for
|
||||
from engineering.mech.xdata import APP_ID, to_values
|
||||
|
||||
b = await _b()
|
||||
payload = {
|
||||
"v": 1,
|
||||
"kind": "part",
|
||||
"id": "P1",
|
||||
"view": "side",
|
||||
"part": {
|
||||
"name": "STUD",
|
||||
"material": "steel",
|
||||
"segments": [{"length": 60.0, "d_outer": 20.0}],
|
||||
"features": [{"kind": "thread", "id": "t1", "designation": "M20x1.5"}],
|
||||
},
|
||||
}
|
||||
anchor = await b.entity_create_circle(0.0, 0.0, 0.5, layer="0")
|
||||
await b.entity_set_xdata(anchor.handle, APP_ID, to_values(payload))
|
||||
await b.entity_create_circle(0.0, 0.0, 10.0, layer="GEOMETRY")
|
||||
before = issues_for("mech_thread_unrepresented", await build_index(b))
|
||||
await b.entity_create_arc(0.0, 0.0, 8.0, 0.0, 270.0, layer="GEOMETRY")
|
||||
after = issues_for("mech_thread_unrepresented", await build_index(b))
|
||||
return len(before) == 1 and after == []
|
||||
|
||||
|
||||
async def sheet_frame_iso5457_a3():
|
||||
"""ISO 5457 A3: 420 x 297 trimmed, a 20 mm filing margin on the left and
|
||||
10 mm on the other three edges, so the frame runs (20, 10) to (410, 287)
|
||||
and the drawing space is 390 x 277. The trimmed sheet edge is drawn too
|
||||
(ISO 5457 4.1), so the outermost extent is the sheet, 0,0 to 420,297 -
|
||||
and nothing may reach past it."""
|
||||
from engineering.mech.primitives import points_of
|
||||
from engineering.sheet.frames import SHEETS, frame_prims
|
||||
|
||||
if tuple(float(v) for v in SHEETS["A3"]) != (297.0, 420.0):
|
||||
return False
|
||||
prims = frame_prims("A3", orientation="landscape", zones=False, marks=False)
|
||||
points = {
|
||||
(round(float(x), 6), round(float(y), 6)) for prim in prims for x, y in points_of(prim)
|
||||
}
|
||||
frame = {(20.0, 10.0), (410.0, 10.0), (410.0, 287.0), (20.0, 287.0)}
|
||||
sheet = {(0.0, 0.0), (420.0, 0.0), (420.0, 297.0), (0.0, 297.0)}
|
||||
xs = [x for x, _y in points]
|
||||
ys = [y for _x, y in points]
|
||||
extent = (min(xs), min(ys), max(xs), max(ys))
|
||||
return frame <= points and sheet <= points and extent == (0.0, 0.0, 420.0, 297.0)
|
||||
|
||||
|
||||
async def bom_balloon_link():
|
||||
"""Two identical bolts collapse into one row of quantity 2, the item number
|
||||
is stable across a rerun, and the balloon drawn for that row carries it."""
|
||||
from engineering.mech.stdparts import insert_std_part
|
||||
from engineering.mech.xdata import APP_ID, decode
|
||||
from engineering.sheet.bom import balloon_prims, rows_from_records
|
||||
|
||||
b = await _b()
|
||||
records = []
|
||||
for x in (0.0, 60.0):
|
||||
result = await insert_std_part(b, "ISO 4014 - M12x60", at=(x, 0.0), view="side")
|
||||
raw = await b.entity_get_xdata(_inserted_handle(result), APP_ID)
|
||||
records.append(decode(raw["xdata"][APP_ID]))
|
||||
rows = rows_from_records(records)
|
||||
again = rows_from_records(records)
|
||||
if len(rows) != 1 or len(again) != 1:
|
||||
return False
|
||||
item = _field(rows[0], "item", "no", "pos")
|
||||
if item is None or item != _field(again[0], "item", "no", "pos"):
|
||||
return False
|
||||
if int(_field(rows[0], "qty", "quantity") or 0) != 2:
|
||||
return False
|
||||
texts = [
|
||||
p.text for p in balloon_prims(int(item), (0.0, 40.0), (0.0, 5.0)) if hasattr(p, "text")
|
||||
]
|
||||
return texts == [str(int(item))]
|
||||
|
||||
|
||||
async def std_part_iso4014_m12():
|
||||
"""ISO 4014's M12 row and the block it becomes.
|
||||
|
||||
ISO 4014:2011 table 1, M12: width across flats s = 18 mm, head height
|
||||
k = 7.5 mm. The row is matched by value rather than by column name — the
|
||||
spelling is the catalogue's business, the numbers are the standard's — and
|
||||
the inserted block has to carry its designation back in ACADMCP_MECH.
|
||||
The catalogue nests a row's sizes one level down (``dims``), so the
|
||||
values are read one level deep.
|
||||
"""
|
||||
from engineering.mech.stdparts import catalogue, insert_std_part
|
||||
from engineering.mech.xdata import APP_ID, decode
|
||||
|
||||
rows = [row for row in catalogue("M12") if any("4014" in str(value) for value in row.values())]
|
||||
if not rows:
|
||||
return False
|
||||
values = [
|
||||
inner
|
||||
for row in rows
|
||||
for value in row.values()
|
||||
for inner in (value.values() if isinstance(value, dict) else (value,))
|
||||
]
|
||||
numbers = {
|
||||
round(float(value), 3)
|
||||
for value in values
|
||||
if isinstance(value, (int, float)) and not isinstance(value, bool)
|
||||
}
|
||||
if not {18.0, 7.5} <= numbers:
|
||||
return False
|
||||
b = await _b()
|
||||
result = await insert_std_part(b, "ISO 4014 - M12x60", at=(0.0, 0.0), view="side")
|
||||
raw = await b.entity_get_xdata(_inserted_handle(result), APP_ID)
|
||||
payload = decode(raw["xdata"][APP_ID])
|
||||
return payload["kind"] == "std_part" and "M12" in str(payload["designation"])
|
||||
|
||||
|
||||
CHECKS = {
|
||||
"core_line_length": (core_line_length, "Core"),
|
||||
"core_circle_radius": (core_circle_radius, "Core"),
|
||||
@@ -450,6 +675,13 @@ CHECKS = {
|
||||
"settings_dimstyle_iso25_values": (settings_dimstyle_iso25_values, "Settings"),
|
||||
"settings_layer_state_roundtrip": (settings_layer_state_roundtrip, "Settings"),
|
||||
"settings_pdf_mediabox_a3": (settings_pdf_mediabox_a3, "Settings"),
|
||||
"mech_part_roundtrip": (mech_part_roundtrip, "Mechanical"),
|
||||
"mech_section_hatch_area": (mech_section_hatch_area, "Mechanical"),
|
||||
"mech_iso286_on_dimension": (mech_iso286_on_dimension, "Mechanical"),
|
||||
"mech_thread_unrepresented_is_caught": (mech_thread_unrepresented_is_caught, "Mechanical"),
|
||||
"std_part_iso4014_m12": (std_part_iso4014_m12, "Mechanical"),
|
||||
"sheet_frame_iso5457_a3": (sheet_frame_iso5457_a3, "Sheet"),
|
||||
"bom_balloon_link": (bom_balloon_link, "Sheet"),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from benchmarks.tasks_v4 import TASKS_V4
|
||||
from benchmarks.tasks_v5 import TASKS_V5
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_INPUT_DIR = ROOT / "benchmarks" / "results" / "published"
|
||||
@@ -66,8 +66,8 @@ def render_chart(reports: list[dict[str, Any]], output: Path) -> None:
|
||||
|
||||
matplotlib.rcParams["svg.hashsalt"] = "autocad-mcp-taskmatrix-v1"
|
||||
|
||||
task_ids = [task.task_id for task in TASKS_V4]
|
||||
task_labels = [f"{task.task_id} ({task.category})" for task in TASKS_V4]
|
||||
task_ids = [task.task_id for task in TASKS_V5]
|
||||
task_labels = [f"{task.task_id} ({task.category})" for task in TASKS_V5]
|
||||
servers = [report["adapter"] for report in reports]
|
||||
status_by_server = {
|
||||
report["adapter"]: {item["task_id"]: item["status"] for item in report["results"]}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"summary": {
|
||||
"baseline_ref": "v1.5.1",
|
||||
"total": 32,
|
||||
"total": 39,
|
||||
"old_pass": 26,
|
||||
"new_pass": 32,
|
||||
"old_pct": 81.2,
|
||||
"new_pass": 39,
|
||||
"old_pct": 66.7,
|
||||
"new_pct": 100.0,
|
||||
"fixed": 6,
|
||||
"fixed": 13,
|
||||
"regressed": 0
|
||||
},
|
||||
"rows": [
|
||||
@@ -201,6 +201,48 @@
|
||||
"category": "Settings",
|
||||
"old": "miss",
|
||||
"new": "pass"
|
||||
},
|
||||
{
|
||||
"check": "mech_part_roundtrip",
|
||||
"category": "Mechanical",
|
||||
"old": "miss",
|
||||
"new": "pass"
|
||||
},
|
||||
{
|
||||
"check": "mech_section_hatch_area",
|
||||
"category": "Mechanical",
|
||||
"old": "miss",
|
||||
"new": "pass"
|
||||
},
|
||||
{
|
||||
"check": "mech_iso286_on_dimension",
|
||||
"category": "Mechanical",
|
||||
"old": "miss",
|
||||
"new": "pass"
|
||||
},
|
||||
{
|
||||
"check": "mech_thread_unrepresented_is_caught",
|
||||
"category": "Mechanical",
|
||||
"old": "miss",
|
||||
"new": "pass"
|
||||
},
|
||||
{
|
||||
"check": "std_part_iso4014_m12",
|
||||
"category": "Mechanical",
|
||||
"old": "miss",
|
||||
"new": "pass"
|
||||
},
|
||||
{
|
||||
"check": "sheet_frame_iso5457_a3",
|
||||
"category": "Sheet",
|
||||
"old": "miss",
|
||||
"new": "pass"
|
||||
},
|
||||
{
|
||||
"check": "bom_balloon_link",
|
||||
"category": "Sheet",
|
||||
"old": "miss",
|
||||
"new": "pass"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -16,7 +16,7 @@ from pathlib import Path
|
||||
|
||||
from backends.capability import UnsupportedCapabilityError
|
||||
from benchmarks.adapters.base import BenchmarkAdapter, TaskResult
|
||||
from benchmarks.tasks_v4 import DEFAULT_MATRIX, MATRICES, TaskSpec, task_by_id
|
||||
from benchmarks.tasks_v5 import DEFAULT_MATRIX, MATRICES, TaskSpec, task_by_id
|
||||
|
||||
|
||||
def _load_registry() -> dict[str, dict]:
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
"""The v5 task matrix: v4 plus what tracks B and G added.
|
||||
|
||||
``mech_assembly`` (tracks B + G) is roadmap criterion 3: a flange-coupling
|
||||
sheet built from the tools — two parts with a section, a bolt circle of ISO
|
||||
4014 bolts and ISO 4032 nuts, ISO 129 dimensions, an ISO 5457 A3 frame, an
|
||||
ISO 7200 title block and an ISO 7573 parts list with balloons. The gate is two
|
||||
numbers, not an impression: ``drawing_critique(focus=None)`` returns zero
|
||||
issues and the finalize score is at least 90. Either one slipping fails the
|
||||
task outright.
|
||||
|
||||
The published competitor reports were never asked this question; the chart
|
||||
shows ``not_run`` for them, not zero.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from benchmarks.tasks_v2 import TASKS_V2
|
||||
from benchmarks.tasks_v3 import TASKS_V3
|
||||
from benchmarks.tasks_v4 import TASKS_V4, TaskSpec
|
||||
|
||||
#: Added in v1.6.0 by tracks B + G.
|
||||
NEW_TASKS_V5: tuple[TaskSpec, ...] = (
|
||||
TaskSpec(
|
||||
"mech_assembly",
|
||||
"mech",
|
||||
"Draw a flange-coupling sheet from the part model and the sheet standard, "
|
||||
"with a clean critique and a finalize score of at least 90",
|
||||
1.0,
|
||||
),
|
||||
)
|
||||
|
||||
TASKS_V5: tuple[TaskSpec, ...] = TASKS_V4 + NEW_TASKS_V5
|
||||
|
||||
#: Runner-selectable task sets. v2, v3 and v4 stay addressable so an earlier
|
||||
#: report can be reproduced exactly rather than only described.
|
||||
MATRICES: dict[str, tuple[TaskSpec, ...]] = {
|
||||
"v2": TASKS_V2,
|
||||
"v3": TASKS_V3,
|
||||
"v4": TASKS_V4,
|
||||
"v5": TASKS_V5,
|
||||
}
|
||||
|
||||
DEFAULT_MATRIX = "v5"
|
||||
|
||||
|
||||
def task_by_id(task_id: str, matrix: str = DEFAULT_MATRIX) -> TaskSpec:
|
||||
for task in MATRICES[matrix]:
|
||||
if task.task_id == task_id:
|
||||
return task
|
||||
raise KeyError(f"{task_id} is not in matrix {matrix}")
|
||||
@@ -488,6 +488,7 @@ async def idle_lane(counter: Any, turns: int = SESSION_TURNS) -> list[dict[str,
|
||||
"""What a client pays on connect, for each advertised surface."""
|
||||
srv = _load_server()
|
||||
previous_profile = config.settings.tool_profile
|
||||
previous_packs = config.settings.tool_packs
|
||||
rows: list[dict[str, Any]] = []
|
||||
try:
|
||||
config.settings.tool_profile = "full"
|
||||
@@ -510,6 +511,17 @@ async def idle_lane(counter: Any, turns: int = SESSION_TURNS) -> list[dict[str,
|
||||
)
|
||||
)
|
||||
config.settings.tool_profile = "full"
|
||||
config.settings.tool_packs = "core,mech"
|
||||
rows.append(
|
||||
await _idle_row(
|
||||
"packs_core_mech",
|
||||
"TOOL_PACKS=core,mech - a mechanical client that never opens a "
|
||||
"P&ID or a live seat.",
|
||||
counter,
|
||||
turns,
|
||||
)
|
||||
)
|
||||
config.settings.tool_packs = previous_packs
|
||||
srv._apply_discovery_mode("search")
|
||||
rows.append(
|
||||
await _idle_row(
|
||||
@@ -523,6 +535,9 @@ async def idle_lane(counter: Any, turns: int = SESSION_TURNS) -> list[dict[str,
|
||||
finally:
|
||||
srv._apply_discovery_mode("off")
|
||||
config.settings.tool_profile = previous_profile
|
||||
# restored here too, so a failed measurement cannot leave the process
|
||||
# on a narrowed surface
|
||||
config.settings.tool_packs = previous_packs
|
||||
await srv._apply_tool_profile(previous_profile)
|
||||
return rows
|
||||
|
||||
|
||||
Reference in New Issue
Block a user