Files
autocad-mcp-pro/engineering/pid/symbols_vessels.py
T

236 lines
8.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# engineering/pid/symbols_vessels.py
"""Parametric vessels (ISO 10628-2): size and nozzles come from the caller.
Unlike the fixed-size catalogue, a vessel's block definition depends on its
parameters, so the block name carries an 8-hex hash of the validated parameter
set (spec 4.5): identical parameters share one definition, different ones get
their own. Nozzles become stub lines plus named ports; the default set is
``N1`` top, ``N2`` bottom, ``N3`` left (0.7 up the wall), ``N4`` right (0.3).
"""
from __future__ import annotations
from .geometry import arc, line, polyline
from .symbols import Port, SymbolSpec, make_spec, params_hash, register, tag_attdefs
VESSELS = ("vertical_vessel", "horizontal_vessel", "tank", "column", "reactor", "hopper")
SIDES = ("top", "bottom", "left", "right")
DEFAULT_NOZZLES = [
{"name": "N1", "side": "top", "fraction": 0.5},
{"name": "N2", "side": "bottom", "fraction": 0.5},
{"name": "N3", "side": "left", "fraction": 0.7},
{"name": "N4", "side": "right", "fraction": 0.3},
]
_DEFAULTS: dict[str, dict] = {
"vertical_vessel": {"width": 20.0, "height": 40.0},
"horizontal_vessel": {"width": 40.0, "height": 20.0},
"tank": {"width": 24.0, "height": 24.0, "roof": "flat"},
"column": {"width": 16.0, "height": 60.0, "trays": 0},
"reactor": {"width": 24.0, "height": 36.0, "jacketed": False},
"hopper": {"width": 24.0, "height": 24.0},
}
PARAMS_SCHEMA = {
"width": "float > 0 (mm)",
"height": "float > 0 (mm)",
"nozzles": "[{name, side: top|bottom|left|right, fraction: 0..1}]",
"roof": "tank only: flat | cone",
"trays": "column only: int >= 0",
"jacketed": "reactor only: bool",
}
#: Nozzle stub length beyond the vessel wall (symbol-local mm).
STUB = 3.0
#: Gap between a jacketed reactor's wall and its jacket line.
JACKET = 2.0
def _is_number(value) -> bool:
return isinstance(value, (int, float)) and not isinstance(value, bool)
def validate_vessel_params(symbol: str, params: dict | None) -> dict:
"""Merge ``params`` over the vessel's defaults and validate every key.
Raises ``ValueError`` naming the offending key (and nozzle index) before
anything is built; the returned dict is the canonical parameter set the
block name is hashed from.
"""
if symbol not in VESSELS:
raise ValueError(f"unknown vessel {symbol!r}; valid: {', '.join(VESSELS)}")
merged = dict(_DEFAULTS[symbol])
merged["nozzles"] = [dict(n) for n in DEFAULT_NOZZLES]
if symbol == "hopper":
merged["nozzles"] = [dict(n) for n in DEFAULT_NOZZLES[:2]]
if params is not None and not isinstance(params, dict):
raise ValueError(f"{symbol}: params must be a dict")
for key, value in (params or {}).items():
if key not in merged:
raise ValueError(
f"{symbol}: unknown parameter {key!r}; valid: {', '.join(sorted(merged))}"
)
merged[key] = value
for key in ("width", "height"):
v = merged[key]
if not _is_number(v) or v <= 0:
raise ValueError(f"{symbol}: {key} must be a number > 0")
merged[key] = float(v)
if "roof" in merged and merged["roof"] not in ("flat", "cone"):
raise ValueError(f"{symbol}: roof must be 'flat' or 'cone'")
if "trays" in merged:
trays = merged["trays"]
if isinstance(trays, bool) or not isinstance(trays, int) or trays < 0:
raise ValueError(f"{symbol}: trays must be an int >= 0")
if "jacketed" in merged and not isinstance(merged["jacketed"], bool):
raise ValueError(f"{symbol}: jacketed must be a bool")
nozzles = merged["nozzles"]
if not isinstance(nozzles, list):
raise ValueError(f"{symbol}: nozzles must be a list")
names: set[str] = set()
clean = []
for i, n in enumerate(nozzles):
if not isinstance(n, dict) or not isinstance(n.get("name"), str) or not n["name"]:
raise ValueError(f"{symbol}: nozzles[{i}].name must be a non-empty string")
if n["name"] in names:
raise ValueError(f"{symbol}: nozzles[{i}].name {n['name']!r} is repeated")
names.add(n["name"])
if n.get("side") not in SIDES:
raise ValueError(f"{symbol}: nozzles[{i}].side must be one of {', '.join(SIDES)}")
f = n.get("fraction", 0.5)
if not _is_number(f) or not 0.0 <= f <= 1.0:
raise ValueError(f"{symbol}: nozzles[{i}].fraction must be within 0..1")
clean.append({"name": n["name"], "side": n["side"], "fraction": float(f)})
merged["nozzles"] = clean
return merged
def _nozzles(
params: dict, half_w: float, half_h: float, extra: float = 0.0
) -> tuple[list[dict], list[Port]]:
"""Nozzle stub primitives and ports on a box of ±half_w × ±half_h.
``extra`` lengthens the stubs (a jacketed reactor's nozzles pass through
the jacket) so every port still sits on the symbol's outer edge.
"""
prims: list[dict] = []
ports: list[Port] = []
stub = STUB + extra
for n in params["nozzles"]:
side, f = n["side"], n["fraction"]
if side == "top":
x, y0, y1 = -half_w + f * 2 * half_w, half_h, half_h + stub
prims.append(line(x, y0, x, y1))
ports.append(Port(n["name"], x, y1, 90.0))
elif side == "bottom":
x, y0, y1 = -half_w + f * 2 * half_w, -half_h, -half_h - stub
prims.append(line(x, y0, x, y1))
ports.append(Port(n["name"], x, y1, 270.0))
elif side == "left":
y, x0, x1 = -half_h + f * 2 * half_h, -half_w, -half_w - stub
prims.append(line(x0, y, x1, y))
ports.append(Port(n["name"], x1, y, 180.0))
else: # right
y, x0, x1 = -half_h + f * 2 * half_h, half_w, half_w + stub
prims.append(line(x0, y, x1, y))
ports.append(Port(n["name"], x1, y, 0.0))
return prims, ports
def _capsule_vertical(w: float, h: float) -> list[dict]:
"""Cylinder with dished ends, axis vertical: two walls and two half-circle heads."""
r = w / 2
body = h / 2 - r
return [
line(-r, -body, -r, body),
line(r, -body, r, body),
arc(0, body, r, 0, 180),
arc(0, -body, r, 180, 360),
]
def _capsule_horizontal(w: float, h: float) -> list[dict]:
r = h / 2
body = w / 2 - r
return [
line(-body, -r, body, -r),
line(-body, r, body, r),
arc(body, 0, r, 270, 90),
arc(-body, 0, r, 90, 270),
]
def build_vessel(symbol: str = "vertical_vessel", params: dict | None = None) -> SymbolSpec:
p = validate_vessel_params(symbol, params)
w, h = p["width"], p["height"]
half_w, half_h = w / 2, h / 2
extra = 0.0
if symbol == "vertical_vessel":
prims = _capsule_vertical(w, h)
elif symbol == "horizontal_vessel":
prims = _capsule_horizontal(w, h)
elif symbol == "tank":
roof = w / 6 if p["roof"] == "cone" else 0.0
prims = [
polyline(
[
(-half_w, -half_h),
(half_w, -half_h),
(half_w, half_h - roof),
(-half_w, half_h - roof),
]
)
]
if roof:
prims.append(
polyline(
[(-half_w, half_h - roof), (0, half_h), (half_w, half_h - roof)],
closed=False,
)
)
elif symbol == "column":
prims = _capsule_vertical(w, h)
trays = p["trays"]
for i in range(trays):
y = -half_h + (i + 1) * h / (trays + 1)
prims.append(line(-half_w + 1, y, half_w - 1, y))
elif symbol == "reactor":
prims = _capsule_vertical(w, h)
if p["jacketed"]:
body = half_h - half_w
prims += [
line(-half_w - JACKET, -body, -half_w - JACKET, body),
line(half_w + JACKET, -body, half_w + JACKET, body),
]
extra = JACKET
else: # hopper: flat top, walls converging to a narrow outlet
prims = [
polyline([(-half_w, half_h), (half_w, half_h), (w / 6, -half_h), (-w / 6, -half_h)])
]
nozzle_prims, ports = _nozzles(p, half_w, half_h, extra)
prims += nozzle_prims
xs = [-half_w - extra, half_w + extra] + [pt.x for pt in ports]
ys = [-half_h, half_h] + [pt.y for pt in ports]
box = (min(xs), min(ys), max(xs), max(ys))
return make_spec(
f"PID_VESSEL_{symbol.upper()}_{params_hash(p)}",
"vessel",
symbol,
None,
prims,
tag_attdefs(box[3]),
ports,
"ISO 10628-2:2012 vessels",
bbox=box,
parametric=True,
params=p,
)
def _vessel_builder(symbol: str):
def build(params: dict | None = None) -> SymbolSpec:
return build_vessel(symbol, params)
return build
for _symbol in VESSELS:
register(_symbol, "vessel", _vessel_builder(_symbol), params_schema=PARAMS_SCHEMA)