docs: track H (understanding foreign drawings) design spec

Read a drawing somebody else made and turn it into a deliverable: a one-call
drawing report (units that lie, stray extents, plan copies, multilingual layer
meaning), a scale check between a P&ID and its layout, pipe and cable takeoffs
whose topology comes from the P&ID and whose lengths come from the layout, a
revision diff by geometric signature, and topology checks. Grounded in a real
client project read locally (never committed): its P&ID proved to be
unscaled against its layout, which no tool had flagged.

Six tools (+ pack plant), three topo_* focuses, the office extra, a live DXF
snapshot through Document.Export (measured: the document is not renamed), and
synthetic fixtures that reproduce the field findings.

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:
Umut CAN
2026-09-24 09:32:31 +03:00
co-authored by Claude Opus 5.5
parent 0abbe55041
commit 18995bf854
@@ -0,0 +1,305 @@
# v1.6 Track H — understanding foreign drawings, takeoffs, diff and topology
**Date:** 2026-09-24 · **Status:** design approved in conversation ·
**Parent:** `2026-09-15-v1.6-roadmap-design.md` (track H; track D stays separate
and follows) · **Baseline:** main after track F (`0abbe55`, 241 tools)
## 1. Summary
Every track so far taught the server to *draw*. This one teaches it to *read* a
drawing somebody else made and turn it into a deliverable — the job a plant
engineer does by hand when a client sends a P&ID and a layout and asks how much
pipe and cable the job needs.
The design is grounded in a real client project from the user's archive, read
on 2026-09-24 (the files are confidential and never enter this repository; see
§14): a process plant with a 35,590-entity P&ID (96 layers, labels in
Russian, layer names in English and Dutch, 296 block names, most anonymous),
a 22,399-entity layout, a hand-made pipe takeoff and a hand-made cable
takeoff. Four findings shape everything below:
1. **Today's reader cannot read it.** `pid_graph` found 441 nodes on that P&ID —
427 of them `unknown_block` at confidence 0.3 — and 663 edges, every one of
line class `unknown`, in 73 s. Its confidences were honest; its answer was
not useful. It cannot tell a CIP supply line from a product line, read a
diameter label, or recognise a Russian service word.
2. **The P&ID is not to scale.** Matching equipment tags between the P&ID and
the layout, distances between the same pairs of tanks differ by factors from
0.1 to 5.6; inside one room the P&ID is a consistent ~1.28× stretch of the
plan, inside another it is rearranged outright. A pipe length measured on
that P&ID is a length of drawn line, not of pipe — and the hand-made pipe
takeoff was measured exactly that way, while the hand-made cable takeoff of
the same project states in its own method sheet that the P&ID is unscaled.
No tool warned anybody. This track makes that check a tool.
3. **The layout lies about its units.** `INSUNITS` says inches; the geometry is
millimetres (a 5.9 km wall total, a 150–280 m building). One stray entity
drags the extents to −6,450 km, and two copies of the plan sit side by side
in model space.
4. **Real drawings are big.** The two DXFs are 55 MB and 188 MB; `drawing_open`
refuses anything over 50 MB, and reading 35,000 entities one COM call at a
time takes minutes where a DXF snapshot takes half a second.
## 2. Scope
**In:** six tools (§12), three `topo_*` critique focuses, the `office` extra
(`openpyxl`), a DXF-snapshot reader for the live engine, the raised size limit,
synthetic fixtures that reproduce the field findings, two benchmark tasks,
correctness checks and a local field-test script.
**Out of 1.6 (recorded):**
- Plant 3D `.dcf` reading (track D, next).
- Cable cross-section *standards*: no IEC 60364-5-52 table is authored. A
section is chosen only from a rule table the caller supplies (the shop's own
rule); without one, no section is proposed. Voltage-drop calculation is out.
- Routing pipes around walls and equipment. A layout length is an orthogonal
estimate between connected equipment (§9) — the method the user's cable
takeoff already uses — and is labelled an estimate.
- OCR of raster underlays and text inside PDF-imported hatch geometry.
## 3. Architecture
```
engineering/understand/
__init__.py
snapshot.py one bulk read of a drawing: ezdxf directly, or a DXF snapshot of
the live document through Document.Export (§4)
vocab.py multilingual keyword tables (EN / TR / RU / NL / DE): disciplines,
services, equipment kinds, room words, supply/return words
labels.py parsers: diameters (Ø51, Ø25,4, SMS51, DN20, 20x27, 1,5"),
equipment tags, electrical data (kW, phases, voltage, +N, VFD)
units.py declared vs inferred units, robust extents, outliers
clusters.py spatial clusters of model-space content (sheets, plan copies)
describe.py drawing_understand: the report assembled from the above
scale.py drawing_scale_check: tag matching and ratio statistics
network.py line networks: junction merge, T-split, fittings bridged, runs,
service and diameter per edge, equipment attachment
route.py orthogonal lengths: Manhattan distance, rectilinear MST
takeoff.py pipe_takeoff and cable_takeoff rows, statuses, totals
report.py the takeoff workbook (Metraj / Özet / Kontrol / Metodoloji), CSV
diff.py entity signatures, matching, the change report
topology.py dangling endpoints, near misses, interior crossings
critique.py the three topo_* focuses
draw.py the async layer: snapshot, markup (revision clouds), exports
scripts/field_test_takeoff.py local only: runs the takeoffs on the user's files
```
Everything except `snapshot.py` and `draw.py` is pure: entity records in,
results out. The entity record is a small frozen dataclass (type, layer,
handle, geometry, text, block name, attributes) filled once per drawing, so a
35,000-entity drawing is read once and every analysis runs in memory.
## 4. Reading a drawing (the snapshot)
- **Headless:** `ezdxf.readfile` on the path, or the document already open.
- **Live:** `Document.Export(<path without extension>, "DXF", <selection set of
all>)` writes a DXF copy **without renaming the document** — measured on
AutoCAD 2026 on 2026-09-24: the document kept its name, an unsaved document
exported too, and ezdxf read the result. The extension argument must be
`"DXF"`; `".dxf"` fails with *Invalid argument* (measured). The snapshot goes
to a temporary folder and is deleted afterwards. Whether paper-space content
and xrefs reach the snapshot is measured by the plan's first live task and
stated in the tool docstrings.
- `MAX_DXF_BYTES` default rises from 50 MB to 512 MB, and the refusal names the
size and the variable (a real layout here was 188 MB).
Every tool in this track is read-only unless it says otherwise (only
`drawing_diff(markup=True)` and the takeoff export write anything, and the
export writes a file, never the drawing).
## 5. `drawing_understand`
One call, one report, every claim with its evidence and a confidence:
- **Units:** declared `INSUNITS` versus the unit the geometry implies (text
heights, typical wall / door sizes, dimension values, overall size). A
disagreement is a warning with the numbers, never a silent correction.
- **Extents:** declared versus robust (quantile) extents; the outlier entities
that stretch them, by handle.
- **Clusters:** separate bodies of content in model space — sheets, plan copies,
details — each with its box, its entity count and its most telling labels.
- **Layers:** every layer with its entity count and drawn length, and the
discipline / service it most likely carries (piping · product, CIP supply,
CIP return, glycol, steam, condensate, water, air, drain; electrical; walls;
equipment; annotation), from the multilingual vocabulary and the texts found
on it; confidence 0.9 for a direct keyword, lower for weaker evidence.
- **Equipment tags**, **rooms** (label words in five languages — *room*, *oda*,
*mahal*, *помещение*, *ruimte* — plus faces of wall layers through track F's
finder), **text languages**, **blocks** (named / anonymous, kinds by name),
**layouts** and **title-block attributes**.
## 6. `drawing_scale_check`
Compares a drawing with a reference drawing of the same plant. Equipment tags
found once in each are matched; for every pair of matched tags at least 2 m
apart the ratio of their distances is taken. The report gives the matched tags,
the median ratio, the interquartile range, the share of pairs within ±10 % of
the median, the same statistics per room or cluster, and a verdict:
`to_scale` (≥ 80 % of pairs within ±10 %; the factor is reported, so a drawing
at a different but uniform scale is recognised), `schematic` (≤ 50 %), or
`partly` in between. Tag labels are not equipment centres; the ±10 % band and
the 2 m floor are there to absorb that, and the report says so.
## 7. Line networks (the shared engine)
Segments (LINE, LWPOLYLINE, POLYLINE, ARC) on the chosen layers become a graph:
endpoints merged within a tolerance, segments split where another ends on them
(T-junctions), and a segment that ends at an inline fitting (an INSERT whose
box it touches — reducer, valve, coupling) continued through it. A connected
component is a **run**.
- **Service** of an edge: the layer's vocabulary class, overridden by supply /
return words within a search distance (`ПОДАЧА`, `ВОЗВРАТ`, `BESLEME`,
`DÖNÜŞ`, `SUPPLY`, `RETURN`, `AANVOER`, `RETOUR`), carried along the run. Layer
alone is not trusted to split CIP supply from return — the field P&ID proved
why.
- **Diameter** of an edge: the diameter label nearest to it within the search
distance (*direct*), else carried along the run from a labelled edge until a
reducer or a conflicting label (*continuity*), else *unassigned*. Labels are
kept exactly as drawn: `Ø51`, `SMS51`, `DN20`, `Ø25,4` and `Ø25` are different
sizes unless the drawing equates them.
- **Equipment attachment:** a run end inside (or within tolerance of) the box of
the geometry a tag labels is attached to that tag.
## 8. `pipe_takeoff`
Rows by room × service × diameter, in the layout of the user's own takeoff
workbook:
- **Lengths.** `length_source="auto"` (default) runs the scale check between
the P&ID and the layout. If the P&ID is `to_scale`, runs are measured on it.
If not, the length of each run is estimated on the layout: the positions of
the tags it connects, joined by a **rectilinear minimum spanning tree**
(Manhattan distance; a two-ended run is |Δx| + |Δy|), plus
`vertical_allowance` per connection (default 0, stated in the report). The
schematic length drawn on the P&ID is kept alongside as a comparison column,
never as the answer. `"pid"` and `"layout"` choose a source and the report
says which was chosen; `"pid"` on a P&ID the scale check calls `schematic` is
refused unless `force=True`. With no layout given, `auto` measures on the P&ID
and every row carries `scale_verified: false`, stated on the method sheet.
An orthogonal route runs along x first, then y — deterministic, and stated.
- **Status:** **A** — every end on a tag found on the layout and the diameter
direct; **B** — diameter by continuity; **C** — not routable (an end not on
equipment, a tag missing from the layout): reported on the *Kontrol* sheet
with its schematic length only, counted in the total and flagged.
- **Rooms:** a routed length is apportioned to the layout rooms its orthogonal
route crosses (room polygons from the layout's wall faces and room labels);
without room polygons, to the room label nearest the run.
- **Scope:** `services` defaults to product + CIP supply + CIP return (the
user's takeoff); utilities are included only on request.
- **Output:** a workbook — *Metraj* (rows with direct and continuity lengths
split, status, allowance, allowance-included), *Özet* (by service, by
diameter, by room), *Kontrol* (unassigned diameters and unroutable runs with
the reason), *Metodoloji* (generated: sources, the scale-check result, the
rules applied, the tolerances) — with Turkish, English or Russian headers
(`lang`), written as XLSX when `openpyxl` is installed (the new `office`
extra) and as CSV always. `allowance` defaults to 0.20 and is its own column,
never mixed into the net length.
## 9. `cable_takeoff`
One row per electrical load, the user's cable method made repeatable:
- **Power and supply** from the P&ID's electrical texts near each tag (`P =
18,5 кВт`, `3 ф. 400 В + N + PE`, `VFD`, in Russian, Turkish and English
spellings). A load with no stated power keeps an empty power cell and an open
item — **power is never invented**.
- **Panel** from the layout's electrical callouts (`Wiring to CP1` and its
variants) or the P&ID; panel names normalised (`CP 1` → `CP-1`).
- **Length:** Manhattan distance on the layout from the tag to its panel, plus
the allowance, rounded up to the metre (`ROUNDUP(length × (1 + allowance))`,
as the user's sheet does). With no layout given, lengths stay empty and each
row gets an open item — the P&ID is not a length source for cables.
- **Room totals** of known power, with the rule for panel packages versus their
child motors stated in the method sheet so no load is counted twice.
- **Section:** only from a caller-supplied `section_rules` table; otherwise
blank.
- Same workbook shape and languages as §8, with an *Open items* list.
## 10. `drawing_diff`
Two revisions of one drawing (paths, or the current document against a path).
Entity signatures (type, layer, geometry rounded to a tolerance; text content
and position; block name, insertion, rotation, scale and attributes) are
matched exactly first, then by handle, then by type + layer + nearest position
within a move tolerance; what is left is added or removed. The report gives
counts by layer and type, the changed entities with what changed (moved by, text
from → to, attribute from → to, radius from → to), block-definition changes and
a capped list per category. `markup=True` draws revision clouds around the
clusters of change on the newer drawing with track G's revision tools — the
only write in the tool, and only on request.
## 11. `drawing_topology_check` and the `topo_*` focuses
Dangling endpoints, near misses (two ends, or an end and a segment, within a
gap tolerance but not joined) and interior crossings (two segments crossing away
from their ends), on the layers given or, by default, on the network layers
`drawing_understand` classifies with confidence ≥ 0.9. The three critique
focuses (`topo_dangling_endpoint`, `topo_near_miss`, `topo_interior_crossing`)
run the same engine under `focus=None`, **excluding layers a semantic focus
already covers** (P&ID line layers → `pid_*`, architectural walls → `arch_*`),
so one defect is never scored twice; on a drawing made entirely by this server
they are silent.
## 12. Tool surface, packs, lean
| Tool | Pack | Writes? |
|---|---|---|
| `drawing_understand` | core (lean) | no |
| `drawing_scale_check` | core | no |
| `drawing_diff` | core | only `markup=True` |
| `drawing_topology_check` | core | no |
| `pipe_takeoff` | `plant` | a workbook file |
| `cable_takeoff` | `plant` | a workbook file |
**+6 → 247 tools.** New pack `plant` (track D's readers join it later). Lean
gains `drawing_understand` (→ 65). Aliases in five languages (metraj, metre
cetveli, boru listesi, kablo listesi, спецификация, ведомость, takeoff, BOQ,
fark, karşılaştır, revizyon farkı, compare, DWGCOMPARE, birim, ölçek, scale
check).
## 13. Refusals
Before any work: a missing or unreadable file, a DXF over the size limit (with
the variable named), `lang` outside tr/en/ru, a non-positive tolerance or
allowance outside 0–1, `length_source="pid"` with a P&ID the scale check calls
schematic *without* `force=True` (the refusal quotes the statistics), an XLSX
export without `openpyxl` (the `xlsx_write` capability, naming the extra; CSV
still written).
## 14. Confidentiality
Client drawings, spreadsheets and anything derived from them that identifies
the client or its business — file names, client and site names, takeoff
quantities — never enter the repository, its tests, its
benchmark results or its commit messages. The fixtures are **synthetic**
(§15). `scripts/field_test_takeoff.py` runs the tools on files the user points
it at and writes its report next to them, outside the repository; its output is
never committed.
## 15. Evidence
- **Synthetic plant pair** (`tests/fixtures/plant_pair.py`): a generator that
builds a schematic P&ID DXF and a layout DXF of one small plant with known
answers — equipment tags present in both, the P&ID deliberately stretched
1.3× in one room and rearranged in another, service layers with English /
Dutch names, Russian supply / return words, `Ø` / `SMS` / `DN` / `a x b`
labels, a reducer, an inline valve, electrical texts in Russian, `Wiring to
CPn` callouts, a layout with `INSUNITS` set to inches over millimetre
geometry, one far outlier entity and two plan copies.
- **Benchmark tasks** (matrix v7, 21 tasks): `takeoff_roundtrip` — the scale
check calls the synthetic P&ID `schematic`, every pipe run's layout length
equals the hand-computed rectilinear MST exactly, diameters and services match
the generator's truth, and the cable rows equal the generator's Manhattan
lengths with the allowance rounded up; `understand_foreign` — units flagged,
the outlier found, both plan copies separated, the service layers classified.
- **Correctness checks** (+5): `scale_check_detects_schematic`,
`pipe_takeoff_rmst_exact`, `cable_takeoff_roundup`, `diff_detects_known_edits`
(a move, a text change, an attribute change, an add and a delete, and nothing
else), `topology_known_defects`.
- **Field test** (local, not committed): `pipe_takeoff` and `cable_takeoff` on
the user's real pair, compared with their hand-made workbooks; the difference
is expected where the hand-made pipe takeoff measured the unscaled P&ID, and
the report explains each row.
- **Live smoke** `scripts/smoke_understand_com.py`: the snapshot, the takeoffs
and the diff on AutoCAD 2026, every document closed.