"""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