diff --git a/engineering/understand/critique.py b/engineering/understand/critique.py index 10040b5..6cd8c57 100644 --- a/engineering/understand/critique.py +++ b/engineering/understand/critique.py @@ -34,12 +34,10 @@ from engineering.layers import PID_LAYERS from engineering.pid.lines import PID_LINE_LAYERS from engineering.plan_spec import Issue from engineering.understand.topology import ( - NETWORK_CONFIDENCE, - NETWORK_DISCIPLINES, network_layers, topology_findings, ) -from engineering.understand.vocab import classify_layer +from engineering.understand.vocab import is_network_layer if TYPE_CHECKING: from backends.base import AutoCADBackend @@ -66,13 +64,7 @@ SEMANTIC_LAYERS: frozenset[str] = frozenset( def _is_candidate(name: str) -> bool: - if name in SEMANTIC_LAYERS: - return False - meaning = classify_layer(name) - return ( - meaning.get("discipline") in NETWORK_DISCIPLINES - and float(meaning.get("confidence") or 0.0) >= NETWORK_CONFIDENCE - ) + return name not in SEMANTIC_LAYERS and is_network_layer(name) def focus_layers(snap) -> list[str]: diff --git a/engineering/understand/network.py b/engineering/understand/network.py index 6529519..ace1d70 100644 --- a/engineering/understand/network.py +++ b/engineering/understand/network.py @@ -72,7 +72,7 @@ from dataclasses import dataclass from engineering.understand.labels import parse_diameters, parse_tag, wiring_target 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__ = [ "EQUIPMENT_SEARCH_SHARE", @@ -262,13 +262,10 @@ def _layer_names(snap: Snapshot) -> list[str]: def default_layers(snap: Snapshot) -> tuple[str, ...]: - """Every layer `classify_layer` files under piping or electrical with confidence >= 0.9.""" - chosen = [] - for name in _layer_names(snap): - found = classify_layer(name) - if found["discipline"] in NETWORK_DISCIPLINES and found["confidence"] >= NETWORK_CONFIDENCE: - chosen.append(name) - return tuple(chosen) + """Every layer of pipes or cables: `vocab.is_network_layer` - piping or electrical + at confidence >= 0.9, and not a layer of valve, pump or tank symbols or of + annotation (sizes, materials, numbers), whose lines are no pipe.""" + return tuple(name for name in _layer_names(snap) if is_network_layer(name)) 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) if not chosen: raise ValueError( - "layers: no layer is classified piping or electrical with confidence >= 0.9; " - f"name the layers to read. The drawing has: {', '.join(known)}" + "layers: no layer is classified piping or electrical with confidence >= 0.9 " + "(symbol and annotation layers left out); name the layers to read. The drawing " + f"has: {', '.join(known)}" ) return chosen names = [str(name) for name in layers] diff --git a/engineering/understand/topology.py b/engineering/understand/topology.py index ba82994..809265c 100644 --- a/engineering/understand/topology.py +++ b/engineering/understand/topology.py @@ -53,7 +53,7 @@ from collections.abc import Iterable from dataclasses import dataclass 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__ = [ "DEFAULT_TOL", @@ -219,7 +219,7 @@ def network_layers(snap: Snapshot) -> list[dict]: for name in sorted(names): meaning = classify_layer(name) 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( { "layer": name, diff --git a/engineering/understand/vocab.py b/engineering/understand/vocab.py index 6803b24..0379baa 100644 --- a/engineering/understand/vocab.py +++ b/engineering/understand/vocab.py @@ -32,6 +32,7 @@ __all__ = [ "classify_layer", "equipment_kind", "fold", + "is_network_layer", "room_label", "supply_return", ] @@ -438,6 +439,43 @@ def room_label(text: str | None) -> dict | 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: """The kind of equipment a block name says it is: ``{"kind", "keyword", "language"}``. diff --git a/tests/test_understand_vocab.py b/tests/test_understand_vocab.py index cd4a058..ffb939d 100644 --- a/tests/test_understand_vocab.py +++ b/tests/test_understand_vocab.py @@ -17,6 +17,7 @@ from engineering.understand.vocab import ( classify_layer, equipment_kind, fold, + is_network_layer, room_label, supply_return, ) @@ -232,3 +233,31 @@ def test_block_names_say_their_kind(block, kind, language): def test_an_anonymous_block_has_no_kind(): assert equipment_kind("*U12") 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