cable_rows reads each load's power, phases, voltage, neutral and VFD from the P&ID's electrical texts near its tag, and its panel from the layout's wiring callouts (else the P&ID's), names normalised CP 1 -> CP-1. The length is the Manhattan distance on the layout from the tag to its panel, and the cable ROUNDUP(length x (1 + allowance)) to the metre, on the length rounded to 1e-6 m so binary noise never buys a metre. Power is never invented: a load with no stated kW keeps an empty cell and an open item. Without a layout the lengths stay empty with an open item per row. Room and panel totals count a panel package's power once, never its children's as well; the rule is on the method sheet. Sections only from the caller's section_rules - no standard table exists here. The workbook gains the cable shape (Metraj, Ozet by room and panel, Kontrol as the open-items list, Metodoloji) in the same three languages; cable_takeoff joins SECTION 27. Inventory regenerated; README snapshot 243 tools (238 advertised); registry snapshot analysis: 14. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FKFStjEFzpGyLq5bMpWxTd
120 lines
3.9 KiB
Python
120 lines
3.9 KiB
Python
"""The async layer of track H's takeoffs: read the drawings, run the pure engine, write the workbook.
|
|
|
|
The only module of the takeoffs that touches a backend, and it only reads:
|
|
`take_snapshot` gives one bulk read of each drawing (a file, or the current
|
|
document), the pure modules compute in a worker thread so a 35,000-entity
|
|
P&ID does not stall the server, and the workbook is a file beside the path the
|
|
caller names. The drawings themselves are never modified.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import math
|
|
|
|
from engineering.understand.network import build_network
|
|
from engineering.understand.report import check_lang, write_workbook
|
|
from engineering.understand.snapshot import take_snapshot
|
|
from engineering.understand.takeoff import (
|
|
cable_rows,
|
|
check_allowance,
|
|
check_pipe_options,
|
|
check_section_rules,
|
|
pipe_rows,
|
|
)
|
|
|
|
__all__ = ["run_cable_takeoff", "run_pipe_takeoff"]
|
|
|
|
|
|
def _check_tol(tol) -> float:
|
|
try:
|
|
value = float(tol)
|
|
except (TypeError, ValueError):
|
|
raise ValueError(f"tol: expected a number, got {tol!r}") from None
|
|
if not math.isfinite(value) or value <= 0.0:
|
|
raise ValueError(f"tol: must be a finite number greater than zero, got {tol!r}")
|
|
return value
|
|
|
|
|
|
async def run_pipe_takeoff(
|
|
backend,
|
|
*,
|
|
pid_path: str | None = None,
|
|
layout_path: str | None = None,
|
|
output: str | None = None,
|
|
lang: str = "en",
|
|
services=None,
|
|
allowance: float = 0.20,
|
|
vertical_allowance: float = 0.0,
|
|
length_source: str = "auto",
|
|
force: bool = False,
|
|
layers=None,
|
|
tol: float = 1.0,
|
|
) -> dict:
|
|
"""`pipe_rows` over a P&ID and an optional layout, plus the workbook when `output` is named.
|
|
|
|
Every option is checked before a drawing is read. `pid_path=None` reads the
|
|
current document as the P&ID. The result is `pipe_rows`'s dict with
|
|
``files`` added: `write_workbook`'s answer, or None when no `output` was named.
|
|
"""
|
|
check_lang(lang)
|
|
options = check_pipe_options(
|
|
services=services,
|
|
allowance=allowance,
|
|
vertical_allowance=vertical_allowance,
|
|
length_source=length_source,
|
|
)
|
|
tol = _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)
|
|
return pipe_rows(
|
|
network,
|
|
pid,
|
|
layout,
|
|
services=options["services"],
|
|
allowance=options["allowance"],
|
|
vertical_allowance=options["vertical_allowance"],
|
|
length_source=options["length_source"],
|
|
force=bool(force),
|
|
)
|
|
|
|
result = await asyncio.to_thread(compute)
|
|
result["files"] = (
|
|
await asyncio.to_thread(write_workbook, result, kind="pipe", lang=lang, path=output)
|
|
if output
|
|
else None
|
|
)
|
|
return result
|
|
|
|
|
|
async def run_cable_takeoff(
|
|
backend,
|
|
*,
|
|
pid_path: str | None = None,
|
|
layout_path: str | None = None,
|
|
output: str | None = None,
|
|
lang: str = "en",
|
|
allowance: float = 0.20,
|
|
section_rules=None,
|
|
) -> dict:
|
|
"""`cable_rows` over a P&ID and an optional layout, plus the workbook when `output` is named."""
|
|
check_lang(lang)
|
|
allowance = check_allowance(allowance)
|
|
rules = check_section_rules(section_rules)
|
|
pid = await take_snapshot(backend, pid_path)
|
|
layout = await take_snapshot(backend, layout_path) if layout_path else None
|
|
result = await asyncio.to_thread(
|
|
cable_rows, pid, layout, allowance=allowance, section_rules=list(rules) or None
|
|
)
|
|
result["files"] = (
|
|
await asyncio.to_thread(write_workbook, result, kind="cable", lang=lang, path=output)
|
|
if output
|
|
else None
|
|
)
|
|
return result
|