153 lines
6.6 KiB
Python
153 lines
6.6 KiB
Python
"""Compile-time variable supply contracts, separate from physical flow direction.
|
|
|
|
Equation ports may participate in a simultaneous solve. Fixed ports (for example
|
|
an Amesim node's reference/branch ports) require complementary local supplies.
|
|
These declarations do not add numerical state or Python evaluation callbacks.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING, Literal, Mapping
|
|
|
|
if TYPE_CHECKING:
|
|
from .ports import PortDefinition
|
|
|
|
|
|
VARIABLE_LABELS = {
|
|
"p": "压力", "T": "温度", "m_flow": "质量流率", "H_flow": "能量流率",
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PortComputation:
|
|
inputs: tuple[str, ...] = ()
|
|
outputs: tuple[str, ...] = ()
|
|
mode: Literal["equation", "fixed"] = "equation"
|
|
# Output p/T aliases an input on another port of the same component.
|
|
reference_port: str | None = None
|
|
|
|
def __post_init__(self) -> None:
|
|
if self.mode not in {"equation", "fixed"}:
|
|
raise ValueError(f"Unknown port computation mode: {self.mode}")
|
|
members = (*self.inputs, *self.outputs)
|
|
if len(set(members)) != len(members) or set(members) - VARIABLE_LABELS.keys():
|
|
raise ValueError("Port computation variables must be unique, supported quantities.")
|
|
if self.reference_port and not {"p", "T"}.issubset(self.outputs):
|
|
raise ValueError("A reference alias must supply pressure and temperature.")
|
|
|
|
def as_dict(self) -> dict[str, object]:
|
|
return {
|
|
"mode": self.mode,
|
|
"inputs": list(self.inputs),
|
|
"outputs": list(self.outputs),
|
|
**({"referencePort": self.reference_port} if self.reference_port else {}),
|
|
}
|
|
|
|
|
|
THERMODYNAMIC_SUPPLY = PortComputation(inputs=("m_flow", "H_flow"), outputs=("p", "T"))
|
|
FLOW_SUPPLY = PortComputation(inputs=("p", "T"), outputs=("m_flow", "H_flow"))
|
|
ZERO_FLOW_SUPPLY = PortComputation(outputs=("m_flow", "H_flow"))
|
|
IMPLICIT_PNEUMATIC = PortComputation()
|
|
NODE_REFERENCE = PortComputation(inputs=("p", "T"), outputs=("m_flow", "H_flow"), mode="fixed")
|
|
NODE_BRANCH = PortComputation(inputs=("m_flow", "H_flow"), outputs=("p", "T"),
|
|
mode="fixed", reference_port="port_2")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PortSupplyIssue:
|
|
code: str
|
|
message: str
|
|
endpoint: tuple[str, str] | None = None
|
|
|
|
|
|
class PortSupplyError(ValueError):
|
|
def __init__(self, issue: PortSupplyIssue):
|
|
self.issue = issue
|
|
super().__init__(f"{issue.code}: {issue.message}")
|
|
|
|
|
|
def port_supply_issue(first: PortDefinition, second: PortDefinition,
|
|
first_label: str | None = None, second_label: str | None = None
|
|
) -> PortSupplyIssue | None:
|
|
"""Check fixed causality; ordinary equation-to-equation links stay legal."""
|
|
if first.kind != second.kind or first.domain != second.domain:
|
|
return None # Domain/type checks own their existing, more specific errors.
|
|
a, b = first.computation, second.computation
|
|
if not any(item and item.mode == "fixed" for item in (a, b)):
|
|
return None
|
|
for consumer, supplier, consumer_label, supplier_label in (
|
|
(a, b, first_label or first.name, second_label or second.name),
|
|
(b, a, second_label or second.name, first_label or first.name),
|
|
):
|
|
if consumer is None:
|
|
continue
|
|
missing = [name for name in consumer.inputs
|
|
if supplier is None or name not in supplier.outputs]
|
|
if missing:
|
|
quantities = "、".join(VARIABLE_LABELS[name] for name in missing)
|
|
return PortSupplyIssue(
|
|
"CONNECTION_VARIABLE_SUPPLY_MISSING",
|
|
f"{consumer_label} 需要对端提供{quantities},但 {supplier_label} 未提供;"
|
|
"请检查参考口与支路口的连接。气体流向反转不会改变这一供需关系。",
|
|
)
|
|
return None
|
|
|
|
|
|
def reference_supply_issues(
|
|
ports: Mapping[tuple[str, str], PortDefinition],
|
|
adjacency: Mapping[tuple[str, str], tuple[str, str]],
|
|
) -> list[PortSupplyIssue]:
|
|
"""Follow declared aliases to reject a reference ring without an origin.
|
|
|
|
This is a supply check, not a whole-system execution scheduler. Reference
|
|
chains are iterative to support deep networks without Python recursion.
|
|
"""
|
|
issues = []
|
|
resolved: dict[str, set[tuple[str, str]]] = {'p': set(), 'T': set()}
|
|
for endpoint, port in ports.items():
|
|
contract = port.computation
|
|
if not contract or contract.mode != "fixed" or not {"p", "T"}.issubset(contract.inputs):
|
|
continue
|
|
if endpoint not in adjacency:
|
|
continue # Existing unconnected-port checks handle incomplete drawings.
|
|
for variable in ("p", "T"):
|
|
current = endpoint
|
|
visited: set[tuple[str, str]] = set()
|
|
chain: list[str] = []
|
|
while True:
|
|
if current in resolved[variable]:
|
|
resolved[variable].update(visited)
|
|
break
|
|
if current in visited:
|
|
issues.append(PortSupplyIssue(
|
|
"REFERENCE_SUPPLY_CYCLE",
|
|
f"{'.'.join(endpoint)} 的{VARIABLE_LABELS[variable]}参考形成循环,"
|
|
f"没有实际提供者:{' → '.join(chain)} → {'.'.join(current)}。",
|
|
endpoint,
|
|
))
|
|
break
|
|
visited.add(current)
|
|
chain.append('.'.join(current))
|
|
supplier = adjacency.get(current)
|
|
if supplier is None:
|
|
issues.append(PortSupplyIssue(
|
|
"REFERENCE_SUPPLY_UNCONNECTED",
|
|
f"{'.'.join(endpoint)} 的{VARIABLE_LABELS[variable]}参考链在 "
|
|
f"{'.'.join(current)} 中断:该参考输入尚未连接。", endpoint,
|
|
))
|
|
break
|
|
supplied = ports.get(supplier)
|
|
supply = supplied.computation if supplied else None
|
|
if supply is None or variable not in supply.outputs:
|
|
# Direct errors are already reported per connection. An
|
|
# indirect failure is explained at that failing connection.
|
|
break
|
|
if supply.reference_port is None:
|
|
resolved[variable].update(visited)
|
|
break
|
|
chain.append('.'.join(supplier))
|
|
current = supplier[0], supply.reference_port
|
|
if current not in ports:
|
|
raise ValueError(f"Invalid reference port declaration: {current}")
|
|
return issues
|