fix(understand): a network layer holds lines — valve, pump and annotation layers are no pipes
The field test ran the takeoffs on a real P&ID pair, kept outside the repository. It showed that the default network layers took every layer the vocabulary files under piping or electrical. That included the valve layers (KLEPPEN, Valve_*), the pump layers (a "product pumps" layer reads as product), and annotation layers such as pipe sizes, materials and valve numbers. Symbol outlines and leader lines became pipe runs. vocab.is_network_layer is now the one rule. A layer is a network layer when it is piping or electrical at 0.9, its name names no equipment (equipment_kind: valve, pump, tank, motor, panel, reducer), and it names no annotation (sizes, materials, numbers, hatching, or an annotation word). build_network's default layers, drawing_topology_check and the topo_* focuses all use it, so they agree on which lines are the network. Sixteen parametrized cases pin it, across symbol, annotation, pipe and cable layers. 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
ba41428c88
commit
307ccc799f
@@ -34,12 +34,10 @@ from engineering.layers import PID_LAYERS
|
|||||||
from engineering.pid.lines import PID_LINE_LAYERS
|
from engineering.pid.lines import PID_LINE_LAYERS
|
||||||
from engineering.plan_spec import Issue
|
from engineering.plan_spec import Issue
|
||||||
from engineering.understand.topology import (
|
from engineering.understand.topology import (
|
||||||
NETWORK_CONFIDENCE,
|
|
||||||
NETWORK_DISCIPLINES,
|
|
||||||
network_layers,
|
network_layers,
|
||||||
topology_findings,
|
topology_findings,
|
||||||
)
|
)
|
||||||
from engineering.understand.vocab import classify_layer
|
from engineering.understand.vocab import is_network_layer
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from backends.base import AutoCADBackend
|
from backends.base import AutoCADBackend
|
||||||
@@ -66,13 +64,7 @@ SEMANTIC_LAYERS: frozenset[str] = frozenset(
|
|||||||
|
|
||||||
|
|
||||||
def _is_candidate(name: str) -> bool:
|
def _is_candidate(name: str) -> bool:
|
||||||
if name in SEMANTIC_LAYERS:
|
return name not in SEMANTIC_LAYERS and is_network_layer(name)
|
||||||
return False
|
|
||||||
meaning = classify_layer(name)
|
|
||||||
return (
|
|
||||||
meaning.get("discipline") in NETWORK_DISCIPLINES
|
|
||||||
and float(meaning.get("confidence") or 0.0) >= NETWORK_CONFIDENCE
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def focus_layers(snap) -> list[str]:
|
def focus_layers(snap) -> list[str]:
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
from engineering.understand.labels import parse_diameters, parse_tag, wiring_target
|
from engineering.understand.labels import parse_diameters, parse_tag, wiring_target
|
||||||
from engineering.understand.snapshot import EntityRecord, Pt, Snapshot
|
from engineering.understand.snapshot import EntityRecord, Pt, Snapshot
|
||||||
from engineering.understand.vocab import classify_layer, fold, supply_return
|
from engineering.understand.vocab import classify_layer, fold, is_network_layer, supply_return
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"EQUIPMENT_SEARCH_SHARE",
|
"EQUIPMENT_SEARCH_SHARE",
|
||||||
@@ -262,13 +262,10 @@ def _layer_names(snap: Snapshot) -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def default_layers(snap: Snapshot) -> tuple[str, ...]:
|
def default_layers(snap: Snapshot) -> tuple[str, ...]:
|
||||||
"""Every layer `classify_layer` files under piping or electrical with confidence >= 0.9."""
|
"""Every layer of pipes or cables: `vocab.is_network_layer` - piping or electrical
|
||||||
chosen = []
|
at confidence >= 0.9, and not a layer of valve, pump or tank symbols or of
|
||||||
for name in _layer_names(snap):
|
annotation (sizes, materials, numbers), whose lines are no pipe."""
|
||||||
found = classify_layer(name)
|
return tuple(name for name in _layer_names(snap) if is_network_layer(name))
|
||||||
if found["discipline"] in NETWORK_DISCIPLINES and found["confidence"] >= NETWORK_CONFIDENCE:
|
|
||||||
chosen.append(name)
|
|
||||||
return tuple(chosen)
|
|
||||||
|
|
||||||
|
|
||||||
def label_search_default(snap: Snapshot, *, tol: float = 1.0) -> float:
|
def label_search_default(snap: Snapshot, *, tol: float = 1.0) -> float:
|
||||||
@@ -413,8 +410,9 @@ def _choose_layers(snap: Snapshot, layers) -> tuple[str, ...]:
|
|||||||
chosen = default_layers(snap)
|
chosen = default_layers(snap)
|
||||||
if not chosen:
|
if not chosen:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"layers: no layer is classified piping or electrical with confidence >= 0.9; "
|
"layers: no layer is classified piping or electrical with confidence >= 0.9 "
|
||||||
f"name the layers to read. The drawing has: {', '.join(known)}"
|
"(symbol and annotation layers left out); name the layers to read. The drawing "
|
||||||
|
f"has: {', '.join(known)}"
|
||||||
)
|
)
|
||||||
return chosen
|
return chosen
|
||||||
names = [str(name) for name in layers]
|
names = [str(name) for name in layers]
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ from collections.abc import Iterable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from engineering.understand.snapshot import EntityRecord, Pt, Snapshot
|
from engineering.understand.snapshot import EntityRecord, Pt, Snapshot
|
||||||
from engineering.understand.vocab import classify_layer
|
from engineering.understand.vocab import classify_layer, is_network_layer
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DEFAULT_TOL",
|
"DEFAULT_TOL",
|
||||||
@@ -219,7 +219,7 @@ def network_layers(snap: Snapshot) -> list[dict]:
|
|||||||
for name in sorted(names):
|
for name in sorted(names):
|
||||||
meaning = classify_layer(name)
|
meaning = classify_layer(name)
|
||||||
confidence = float(meaning.get("confidence") or 0.0)
|
confidence = float(meaning.get("confidence") or 0.0)
|
||||||
if meaning.get("discipline") in NETWORK_DISCIPLINES and confidence >= NETWORK_CONFIDENCE:
|
if is_network_layer(name):
|
||||||
rows.append(
|
rows.append(
|
||||||
{
|
{
|
||||||
"layer": name,
|
"layer": name,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ __all__ = [
|
|||||||
"classify_layer",
|
"classify_layer",
|
||||||
"equipment_kind",
|
"equipment_kind",
|
||||||
"fold",
|
"fold",
|
||||||
|
"is_network_layer",
|
||||||
"room_label",
|
"room_label",
|
||||||
"supply_return",
|
"supply_return",
|
||||||
]
|
]
|
||||||
@@ -438,6 +439,43 @@ def room_label(text: str | None) -> dict | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
#: Words that make a piping or electrical layer hold annotation, not lines: pipe
|
||||||
|
#: sizes, materials, valve or line numbers, hatching. Matched like every other
|
||||||
|
#: keyword here (a word of five letters or more anywhere in the name).
|
||||||
|
_NOT_A_LINE_WORDS = (
|
||||||
|
("annotation", "diameter", "en"),
|
||||||
|
("annotation", "sizes", "en"),
|
||||||
|
("annotation", "size", "en"),
|
||||||
|
("annotation", "material", "en"),
|
||||||
|
("annotation", "number", "en"),
|
||||||
|
("annotation", "nummer", "nl"),
|
||||||
|
("annotation", "arcering", "nl"),
|
||||||
|
("annotation", "hatch", "en"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_network_layer(name: str | None) -> bool:
|
||||||
|
"""A layer whose lines are pipes or cables.
|
||||||
|
|
||||||
|
`classify_layer` files it under piping or electrical at 0.9, and its name
|
||||||
|
says neither that it holds symbols - a valve, pump, tank, motor, panel or
|
||||||
|
reducer (`equipment_kind`), whose outlines are no pipe - nor that it holds
|
||||||
|
annotation: sizes, materials, numbers, hatching, or an annotation word.
|
||||||
|
Field P&IDs keep valves and pumps on their own layers (``KLEPPEN``,
|
||||||
|
``PRODUCT PUMPS``) that a service keyword alone would read as piping.
|
||||||
|
"""
|
||||||
|
meaning = classify_layer(name)
|
||||||
|
if meaning["discipline"] not in ("piping", "electrical") or meaning["confidence"] < DIRECT:
|
||||||
|
return False
|
||||||
|
if equipment_kind(name) is not None:
|
||||||
|
return False
|
||||||
|
folded = fold(name)
|
||||||
|
if _first(_NOT_A_LINE_WORDS, folded) is not None:
|
||||||
|
return False
|
||||||
|
annotation = [row for row in _DISCIPLINE_WORDS if row[0] == "annotation"]
|
||||||
|
return _first(annotation, folded) is None
|
||||||
|
|
||||||
|
|
||||||
def equipment_kind(block_name: str | None) -> dict | None:
|
def equipment_kind(block_name: str | None) -> dict | None:
|
||||||
"""The kind of equipment a block name says it is: ``{"kind", "keyword", "language"}``.
|
"""The kind of equipment a block name says it is: ``{"kind", "keyword", "language"}``.
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from engineering.understand.vocab import (
|
|||||||
classify_layer,
|
classify_layer,
|
||||||
equipment_kind,
|
equipment_kind,
|
||||||
fold,
|
fold,
|
||||||
|
is_network_layer,
|
||||||
room_label,
|
room_label,
|
||||||
supply_return,
|
supply_return,
|
||||||
)
|
)
|
||||||
@@ -232,3 +233,31 @@ def test_block_names_say_their_kind(block, kind, language):
|
|||||||
def test_an_anonymous_block_has_no_kind():
|
def test_an_anonymous_block_has_no_kind():
|
||||||
assert equipment_kind("*U12") is None
|
assert equipment_kind("*U12") is None
|
||||||
assert equipment_kind(None) is None
|
assert equipment_kind(None) is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("name", "network"),
|
||||||
|
[
|
||||||
|
("P_product piping", True),
|
||||||
|
("P_cipsupplyline", True),
|
||||||
|
("E_power", True),
|
||||||
|
# symbols, not lines: the layer names the equipment drawn on it
|
||||||
|
("KLEPPEN", False),
|
||||||
|
("Valve_ball valves", False),
|
||||||
|
("PRODUCT PUMPS", False),
|
||||||
|
("CIP return pumps", False),
|
||||||
|
("Storage tanks", False),
|
||||||
|
("E_panel connections", False),
|
||||||
|
# annotation on a piping layer: sizes, materials, numbers, hatching
|
||||||
|
("P_pipe diameters", False),
|
||||||
|
("PIPESIZES", False),
|
||||||
|
("piping materials", False),
|
||||||
|
("valve numbers", False),
|
||||||
|
("PIPE TEXT", False),
|
||||||
|
# not piping or electrical at all
|
||||||
|
("WALLS", False),
|
||||||
|
("0", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_a_network_layer_holds_lines_not_symbols_or_annotation(name, network):
|
||||||
|
assert is_network_layer(name) is network
|
||||||
|
|||||||
Reference in New Issue
Block a user