fix(understand): the pipe network's tolerance follows the P&ID's unit, and a dropped stub is said

pipe_takeoff merged line ends closer than tol = 1.0 drawing units and
silently dropped any segment shorter than that. On a millimetre P&ID that is
1 mm. On a metre drawing it is a metre: ends a metre apart were merged and
every pipe under a metre disappeared from the totals, with no warning. The
review of the network engine found this.

- tol now defaults to 1 mm in the unit the P&ID's geometry implies: 1.0 on
  a millimetre drawing, 0.001 on a metre one. A tol the caller gives is
  used as given.
- build_network counts the segments that collapse to a point
  (stats.segments_below_tol), and pipe_rows turns a non-zero count into a
  warning naming the tolerance.

A new wire test builds a metre P&ID with a 0.6 m pipe and a 5 m pipe. Before
this change the takeoff said 5.0 m; now it says 5.6 m. The network test
checks that a sub-tolerance stub is counted. Registry, packs, aliases and
every track H suite: 472 passed. The inventory matches the live surface.

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 12:51:10 +03:00
co-authored by Claude Opus 5.5
parent fb2953c324
commit c4759e696d
6 changed files with 51 additions and 7 deletions
+7 -3
View File
@@ -21,6 +21,7 @@ from engineering.understand.takeoff import (
check_pipe_options,
check_section_rules,
pipe_rows,
unit_of,
)
__all__ = ["run_cable_takeoff", "run_pipe_takeoff"]
@@ -49,7 +50,7 @@ async def run_pipe_takeoff(
length_source: str = "auto",
force: bool = False,
layers=None,
tol: float = 1.0,
tol: float | None = None,
) -> dict:
"""`pipe_rows` over a P&ID and an optional layout, plus the workbook when `output` is named.
@@ -64,14 +65,17 @@ async def run_pipe_takeoff(
vertical_allowance=vertical_allowance,
length_source=length_source,
)
tol = _check_tol(tol)
tol = None if tol is None else _check_tol(tol)
if length_source == "layout" and not layout_path:
raise ValueError("length_source='layout' needs a layout drawing; none was given")
pid = await take_snapshot(backend, pid_path)
layout = await take_snapshot(backend, layout_path) if layout_path else None
def compute() -> dict:
network = build_network(pid, layers=layers, tol=tol)
# 1 mm in the unit the P&ID's geometry implies: a fixed 1.0 would be a
# metre on a metre drawing, merging ends a metre apart and dropping pipes
joint = tol if tol is not None else 1.0 / (unit_of(pid)["m_per_unit"] * 1000.0)
network = build_network(pid, layers=layers, tol=joint)
return pipe_rows(
network,
pid,
+4
View File
@@ -539,10 +539,13 @@ def build_network(snap: Snapshot, *, layers=None, tol=1.0, label_search=None) ->
raw = _segments(snap, chosen)
verts = _Vertices(tol)
snapped = []
below_tol = 0 # segments whose two ends merge into one vertex: dropped, and counted
for a, b, bulge, handle, layer in raw:
u, v = verts.index(a), verts.index(b)
if u != v:
snapped.append((u, v, bulge, handle, layer))
else:
below_tol += 1
# equipment first: an INSERT a tag labels is never a fitting
occurrences = tag_occurrences(snap)
@@ -888,6 +891,7 @@ def build_network(snap: Snapshot, *, layers=None, tol=1.0, label_search=None) ->
"tol": tol,
"label_search": search,
"segments": len(raw),
"segments_below_tol": below_tol,
"pieces": len(pieces),
"overlap_removed": overlap_count,
"overlap_length": overlap_length,
+7
View File
@@ -802,6 +802,13 @@ def pipe_rows(
)
}
warnings = [u["warning"] for u in (pid_unit, layout_unit) if u and u["warning"]]
dropped = int(network.get("stats", {}).get("segments_below_tol") or 0)
if dropped:
warnings.append(
f"{dropped} P&ID segment(s) shorter than the junction tolerance "
f"({network['stats']['tol']:g} drawing units) collapsed to a point and are not "
"counted"
)
if not scale_verified:
warnings.append(
"scale_verified: false - lengths are drawn lengths on a P&ID that no scale check "