feat(understand): the snapshot, the vocabulary and the label parsers; MAX_DXF_BYTES 512 MB; the office extra
Track H's foundation, landed on main before its three groups branch. - engineering/understand/snapshot.py: one bulk read of a drawing into frozen WCS records (OCS translated through backends/ocs.py, text decoded, an INSERT boxed by its block geometry without its attribute texts). A DXF is read with ezdxf after the size check; the headless engine's document is read under its own lock; the live engine exports the active document with Document.Export(<tmp>/snap, "DXF", <set of all>) - never renamed, deleted afterwards - and a DWG is opened read-only, exported and closed. Measured on AutoCAD 2026: ".dxf" is refused with Invalid argument, the file lands as snap.DXF, model space, paper space and unused block definitions all reach it, and the selection set does not narrow a DXF export. - engineering/understand/labels.py: %%c, \U+2205, the U+2300 sign, o-stroke and MTEXT format runs all read as one diameter spelling; sizes are kept as drawn and never equated; Cyrillic look-alike capitals fold into Latin tags; panels, wiring callouts and electrical data (kW, phases, volts, N, VFD) in EN / TR / RU / NL / DE. A power that is not written stays None. - engineering/understand/vocab.py: layer classification with a confidence and the deciding keyword, supply / return words, room labels and block kinds in five languages. A CIP layer without a direction word is not guessed. - MAX_DXF_BYTES defaults to 512 MB (a real layout was 188 MB); the refusal names the size and the variable. New `office` extra (openpyxl), also in `full`; uv.lock regenerated. 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
f0c50d772f
commit
9ab0ae1773
@@ -0,0 +1,234 @@
|
||||
"""The multilingual vocabulary: layer names, direction words, room labels, block kinds.
|
||||
|
||||
Every layer name below is one a drafter would write; the ones in the synthetic
|
||||
plant pair (tests/fixtures/plant_pair.py) are here too, because that is what the
|
||||
readers above this module are measured on.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from engineering.understand.vocab import (
|
||||
DISCIPLINES,
|
||||
EQUIPMENT_KINDS,
|
||||
LANGUAGES,
|
||||
SERVICES,
|
||||
classify_layer,
|
||||
equipment_kind,
|
||||
fold,
|
||||
room_label,
|
||||
supply_return,
|
||||
)
|
||||
|
||||
|
||||
def test_the_closed_lists():
|
||||
assert LANGUAGES == ("en", "tr", "ru", "nl", "de")
|
||||
assert SERVICES == (
|
||||
"product",
|
||||
"cip_supply",
|
||||
"cip_return",
|
||||
"glycol_supply",
|
||||
"glycol_return",
|
||||
"steam",
|
||||
"condensate",
|
||||
"cold_water",
|
||||
"hot_water",
|
||||
"ice_water",
|
||||
"compressed_air",
|
||||
"drain",
|
||||
"electrical",
|
||||
"unknown",
|
||||
)
|
||||
assert DISCIPLINES == (
|
||||
"piping",
|
||||
"electrical",
|
||||
"architecture",
|
||||
"equipment",
|
||||
"annotation",
|
||||
"structure",
|
||||
"unknown",
|
||||
)
|
||||
assert EQUIPMENT_KINDS == ("tank", "pump", "valve", "reducer", "motor", "agitator", "panel")
|
||||
|
||||
|
||||
def test_fold_strips_diacritics_and_the_turkish_dotless_i():
|
||||
assert fold("DÖNÜŞ") == "donus"
|
||||
assert fold("GİDİŞ") == "gidis"
|
||||
assert fold("çıkış") == "cikis"
|
||||
assert fold("RückLauf") == "rucklauf"
|
||||
assert fold("ПОДАЧА") == "подача"
|
||||
assert fold(None) == ""
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("name", "service", "keyword", "language"),
|
||||
[
|
||||
("P_product piping", "product", "product", "en"),
|
||||
("P_cipsupplyline", "cip_supply", "cip", "en"),
|
||||
("P_cipreturnline", "cip_return", "cip", "en"),
|
||||
("P_ijswater", "ice_water", "ijswater", "nl"),
|
||||
("CIP ВОЗВРАТ", "cip_return", "cip", "en"),
|
||||
("glikol dönüş", "glycol_return", "glikol", "tr"),
|
||||
("GLYKOL-VORLAUF", "glycol_supply", "glykol", "de"),
|
||||
("buhar hattı", "steam", "buhar", "tr"),
|
||||
("КОНДЕНСАТ", "condensate", "конденсат", "ru"),
|
||||
("P_ICE WATER", "ice_water", "ice water", "en"),
|
||||
("sıcak su", "hot_water", "sicak su", "tr"),
|
||||
("DRUCKLUFT", "compressed_air", "druckluft", "de"),
|
||||
("air_line", "compressed_air", "air", "en"),
|
||||
("RIOOL", "drain", "riool", "nl"),
|
||||
],
|
||||
)
|
||||
def test_service_layers(name, service, keyword, language):
|
||||
row = classify_layer(name)
|
||||
assert row["service"] == service
|
||||
assert row["discipline"] == "piping"
|
||||
assert row["confidence"] == 0.9
|
||||
assert row["keyword"] == keyword
|
||||
assert row["language"] == language
|
||||
|
||||
|
||||
def test_electrical_layers_are_their_own_service():
|
||||
for name in ("E_power", "ELEKTRIK", "ЭЛЕКТРИКА", "kabel"):
|
||||
row = classify_layer(name)
|
||||
assert (row["discipline"], row["service"], row["confidence"]) == (
|
||||
"electrical",
|
||||
"electrical",
|
||||
0.9,
|
||||
), name
|
||||
|
||||
|
||||
def test_a_cip_layer_without_a_direction_is_not_guessed():
|
||||
row = classify_layer("P_CIP")
|
||||
assert row == {
|
||||
"discipline": "piping",
|
||||
"service": "unknown",
|
||||
"confidence": 0.6,
|
||||
"keyword": "cip",
|
||||
"language": "en",
|
||||
}
|
||||
|
||||
|
||||
def test_a_cip_layer_naming_both_directions_is_not_guessed_either():
|
||||
assert classify_layer("CIP supply return")["service"] == "unknown"
|
||||
|
||||
|
||||
def test_a_water_layer_with_no_temperature_is_piping_of_unknown_service():
|
||||
row = classify_layer("P_WATER")
|
||||
assert (row["discipline"], row["service"], row["confidence"]) == ("piping", "unknown", 0.6)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("name", "discipline", "language"),
|
||||
[
|
||||
("KLEPPEN", "piping", "nl"),
|
||||
("VANALAR", "piping", "tr"),
|
||||
("WALLS", "architecture", "en"),
|
||||
("MUREN", "architecture", "nl"),
|
||||
("СТЕНЫ", "architecture", "ru"),
|
||||
("EQUIPMENT", "equipment", "en"),
|
||||
("POMPALAR", "equipment", "tr"),
|
||||
("DIM", "annotation", "en"),
|
||||
("ÖLÇÜ", "annotation", "tr"),
|
||||
("KOLONLAR", "structure", "tr"),
|
||||
],
|
||||
)
|
||||
def test_discipline_layers(name, discipline, language):
|
||||
row = classify_layer(name)
|
||||
assert (row["discipline"], row["service"], row["confidence"], row["language"]) == (
|
||||
discipline,
|
||||
"unknown",
|
||||
0.9,
|
||||
language,
|
||||
)
|
||||
|
||||
|
||||
def test_a_short_keyword_must_start_a_word():
|
||||
assert classify_layer("STAIRS")["service"] != "compressed_air"
|
||||
assert classify_layer("CHAIRS")["discipline"] == "unknown"
|
||||
|
||||
|
||||
def test_a_layer_with_no_keyword():
|
||||
assert classify_layer("0") == {
|
||||
"discipline": "unknown",
|
||||
"service": "unknown",
|
||||
"confidence": 0.0,
|
||||
"keyword": None,
|
||||
"language": None,
|
||||
}
|
||||
assert classify_layer("Defpoints")["discipline"] == "unknown"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("text", "direction"),
|
||||
[
|
||||
("ПОДАЧА", "supply"),
|
||||
("ВОЗВРАТ", "return"),
|
||||
("обратка CIP", "return"), # обратка
|
||||
("BESLEME", "supply"),
|
||||
("DÖNÜŞ", "return"),
|
||||
("donus hatti", "return"),
|
||||
("CIP SUPPLY", "supply"),
|
||||
("return line", "return"),
|
||||
("AANVOER", "supply"),
|
||||
("RETOUR", "return"),
|
||||
("Vorlauf", "supply"),
|
||||
("RÜCKLAUF", "return"),
|
||||
],
|
||||
)
|
||||
def test_supply_and_return_words_in_five_languages(text, direction):
|
||||
assert supply_return(text) == direction
|
||||
|
||||
|
||||
def test_a_text_naming_both_or_neither_direction_is_none():
|
||||
assert supply_return("CIP SUPPLY / RETURN") is None
|
||||
assert supply_return("Ø51") is None
|
||||
assert supply_return(None) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("text", "expected"),
|
||||
[
|
||||
("ROOM 1 PROCESS HALL", {"number": "1", "name": "PROCESS HALL", "language": "en"}),
|
||||
("ODA 2", {"number": "2", "name": None, "language": "tr"}),
|
||||
("MAHAL 1.05 SOĞUK ODA", {"number": "1.05", "name": "SOĞUK ODA", "language": "tr"}),
|
||||
(
|
||||
"ПОМЕЩЕНИЕ 2 МОЙКА",
|
||||
{"number": "2", "name": "МОЙКА", "language": "ru"},
|
||||
),
|
||||
("RUIMTE 3 opslag", {"number": "3", "name": "opslag", "language": "nl"}),
|
||||
("Raum 4", {"number": "4", "name": None, "language": "de"}),
|
||||
],
|
||||
)
|
||||
def test_room_labels_in_five_languages(text, expected):
|
||||
assert room_label(text) == expected
|
||||
|
||||
|
||||
def test_a_text_without_a_room_word_is_not_a_room_label():
|
||||
assert room_label("T101") is None
|
||||
assert room_label("PROCESS HALL") is None
|
||||
assert room_label("") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("block", "kind", "language"),
|
||||
[
|
||||
("KLEP_VLINDER", "valve", "nl"),
|
||||
("VERLOOPSTUK", "reducer", "nl"),
|
||||
("REDUCER_CONC", "reducer", "en"),
|
||||
("TANK_V", "tank", "en"),
|
||||
("POMP", "pump", "nl"),
|
||||
("SCHAKELKAST", "panel", "nl"),
|
||||
("НАСОС", "pump", "ru"),
|
||||
("VANA_KELEBEK", "valve", "tr"),
|
||||
],
|
||||
)
|
||||
def test_block_names_say_their_kind(block, kind, language):
|
||||
row = equipment_kind(block)
|
||||
assert row["kind"] == kind and row["language"] == language
|
||||
|
||||
|
||||
def test_an_anonymous_block_has_no_kind():
|
||||
assert equipment_kind("*U12") is None
|
||||
assert equipment_kind(None) is None
|
||||
Reference in New Issue
Block a user