The evidence for track H, in the shape tracks A, E, B + G and F used.
- benchmarks/tasks_v7.py: 21 tasks, DEFAULT_MATRIX = "v7"; v6 to v2 stay
addressable. Both new tasks read the synthetic plant pair and nothing
else. takeoff_roundtrip: the P&ID called schematic, every routable run's
layout length equal to the rectilinear MST of its tags, sizes and services
as drawn (a return drawn on the supply layer included), the cable metres
rounded up after the allowance, an unstated power left empty.
understand_foreign: inches over millimetres flagged, the stray entity found
by handle, two plan copies separated, five layers classified. A test breaks
each input and watches its gate fail by name.
- Five correctness checks, every number worked out by hand in its docstring;
the published A/B report is re-run over 48 checks: 26 / 48 -> 48 / 48,
zero regressed.
- scripts/smoke_understand_com.py reads the pair through the live engine -
snapshots, the DWG route, both takeoffs and a diff - and closes every
document it opens; the CHANGELOG records what it returned.
scripts/field_test_takeoff.py runs the takeoffs on a user's own files,
refuses to write inside the repository and prints no quantity by default.
- The README token table is remeasured; check_doc_numbers derives the task
count from the v7 matrix; CLAUDE.md, both READMEs, the CHANGELOG and the
spec status line say what shipped.
One reader defect found on the way: describe.tag_occurrences read a wiring
callout ("Wiring to CP-2") as a place its panel is written, so the scale
check called CP-2 ambiguous and read "Wiring to CP1" as a phantom tag CP1 -
10 tags and 45 pairs (13.3 % within 10 %) where the generator placed 11 and
55 (21.8 %). A callout is no longer a tag occurrence, the rule
network.tag_occurrences already kept; two tests failed before the fix.
The smoke works only in documents it opened from its own temporary folder,
known by full path; it attaches with GetActiveObject only (the backend's
Dispatch fallback would launch AutoCAD), checks in the same COM-thread call
that the active document is its own before each snapshot, the DWG save and
the move, closes its documents with Close(False) and re-activates the
document that was active when it started. No new ActiveX member: Documents,
Item, FullName, ActiveDocument, Activate and Close are the ones the backend
already calls. Measured on AutoCAD 2026 (25.1s): exit 0, model-space types
equal file / live / DWG, export 0.104 s, no document of the run left open.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
129 lines
4.8 KiB
Python
129 lines
4.8 KiB
Python
"""Run both takeoffs on a real P&ID and layout, and write the report next to them.
|
|
|
|
LOCAL ONLY. This is the field test of track H (spec §15): the user points it at
|
|
a client's P&ID and layout, compares what comes out with the hand-made
|
|
workbooks, and keeps the result on their own disk. Nothing it reads or writes
|
|
may enter this repository (spec §14), so:
|
|
|
|
* the report is written next to the P&ID (or into ``--out``) and **the script
|
|
refuses any output folder inside this repository**, before reading a byte;
|
|
* the console gets file names, row counts and the scale verdict - **never a
|
|
quantity**, unless ``--show`` is given on the user's own terminal;
|
|
* it is never run in CI and never by a test on a real drawing; the test next
|
|
to it (tests/test_field_test_takeoff.py) runs it on the synthetic plant pair.
|
|
|
|
What it writes: ``pipe_takeoff.xlsx`` / ``cable_takeoff.xlsx`` (CSV beside
|
|
them always, XLSX when the ``office`` extra is installed) through the same
|
|
``write_workbook`` the tools use, and ``field_test_report.json`` with the scale
|
|
check and both results in full.
|
|
|
|
uv run --frozen python scripts/field_test_takeoff.py --pid D:/job/pid.dxf --layout D:/job/layout.dxf
|
|
uv run --frozen python scripts/field_test_takeoff.py --pid ... --layout ... --lang en --show
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
#: Exit code for a refused run (output inside the repository, a missing file).
|
|
REFUSED = 2
|
|
|
|
|
|
def _inside_repository(folder: Path) -> bool:
|
|
resolved = folder.resolve()
|
|
return resolved == ROOT or ROOT in resolved.parents
|
|
|
|
|
|
def _parse(argv) -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Pipe and cable takeoffs on a real P&ID + layout (local only)."
|
|
)
|
|
parser.add_argument("--pid", required=True, help="the P&ID, a .dxf")
|
|
parser.add_argument("--layout", required=True, help="the layout, a .dxf")
|
|
parser.add_argument("--out", default=None, help="report folder (default: the P&ID's folder)")
|
|
parser.add_argument("--lang", default="tr", choices=("tr", "en", "ru"))
|
|
parser.add_argument("--allowance", type=float, default=0.20)
|
|
parser.add_argument(
|
|
"--show", action="store_true", help="also print the totals (quantities) to the console"
|
|
)
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def main(argv=None) -> int:
|
|
args = _parse(argv)
|
|
pid_path, layout_path = Path(args.pid), Path(args.layout)
|
|
out = Path(args.out) if args.out else pid_path.resolve().parent
|
|
if _inside_repository(out):
|
|
print(
|
|
f"refused: {out} is inside the repository ({ROOT}); client drawings and "
|
|
"anything derived from them never enter it. Pass --out outside it."
|
|
)
|
|
return REFUSED
|
|
for path in (pid_path, layout_path):
|
|
if not path.is_file():
|
|
print(f"refused: {path} does not exist")
|
|
return REFUSED
|
|
|
|
from engineering.understand.network import build_network
|
|
from engineering.understand.report import write_workbook
|
|
from engineering.understand.scale import scale_check
|
|
from engineering.understand.snapshot import read_snapshot
|
|
from engineering.understand.takeoff import cable_rows, pipe_rows
|
|
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
pid = read_snapshot(str(pid_path))
|
|
layout = read_snapshot(str(layout_path))
|
|
scale = scale_check(pid, layout)
|
|
pipe = pipe_rows(build_network(pid), pid, layout, scale=scale, allowance=args.allowance)
|
|
cable = cable_rows(pid, layout, allowance=args.allowance)
|
|
written = [
|
|
write_workbook(pipe, kind="pipe", lang=args.lang, path=str(out / "pipe_takeoff.xlsx")),
|
|
write_workbook(cable, kind="cable", lang=args.lang, path=str(out / "cable_takeoff.xlsx")),
|
|
]
|
|
report = out / "field_test_report.json"
|
|
report.write_text(
|
|
json.dumps(
|
|
{"scale": scale, "pipe": pipe, "cable": cable, "written": written},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
default=str,
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"scale_verdict": scale["verdict"],
|
|
"pipe_rows": len(pipe["rows"]),
|
|
"cable_rows": len(cable["rows"]),
|
|
"report": str(report),
|
|
"workbooks": [
|
|
path
|
|
for item in written
|
|
for path in ([item["xlsx"]] if item["xlsx"] else []) + list(item["csv"])
|
|
],
|
|
},
|
|
indent=2,
|
|
)
|
|
)
|
|
if args.show:
|
|
print(
|
|
json.dumps(
|
|
{"pipe": pipe["rows"], "cable": cable["rows"]},
|
|
indent=2,
|
|
ensure_ascii=False,
|
|
default=str,
|
|
)
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|