完善 XML 通用仿真与结果查看
This commit is contained in:
1 parent
104d41d294
commit
f1256a121d
36 files changed
+6666
-196
No files matched your search
+241
-17
@@ -3,14 +3,67 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
|
||||
from PythonModels.core.base import Component, DynamicComponent
|
||||
from PythonModels.core.equations import EquationResidual
|
||||
from PythonModels.core.ports import PortState
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Endpoint:
|
||||
component: str
|
||||
port: str
|
||||
|
||||
@property
|
||||
def key(self) -> tuple[str, str]:
|
||||
return self.component, self.port
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.component}.{self.port}"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Connection:
|
||||
source_component: str
|
||||
source_port: str
|
||||
target_component: str
|
||||
target_port: str
|
||||
id: str
|
||||
kind: str
|
||||
domain: str
|
||||
endpoint_a: Endpoint
|
||||
endpoint_b: Endpoint
|
||||
|
||||
@property
|
||||
def endpoints(self) -> tuple[Endpoint, Endpoint]:
|
||||
return self.endpoint_a, self.endpoint_b
|
||||
|
||||
@property
|
||||
def undirected_key(self) -> tuple[tuple[str, str], tuple[str, str]]:
|
||||
first, second = sorted((self.endpoint_a.key, self.endpoint_b.key))
|
||||
return first, second
|
||||
|
||||
# Compatibility accessors for existing reports. They do not imply physical flow.
|
||||
@property
|
||||
def source_component(self) -> str:
|
||||
return self.endpoint_a.component
|
||||
|
||||
@property
|
||||
def source_port(self) -> str:
|
||||
return self.endpoint_a.port
|
||||
|
||||
@property
|
||||
def target_component(self) -> str:
|
||||
return self.endpoint_b.component
|
||||
|
||||
@property
|
||||
def target_port(self) -> str:
|
||||
return self.endpoint_b.port
|
||||
|
||||
def as_interface_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"kind": self.kind,
|
||||
"domain": self.domain,
|
||||
"endpoints": [
|
||||
{"component": endpoint.component, "port": endpoint.port}
|
||||
for endpoint in self.endpoints
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class SimulationNetwork:
|
||||
@@ -28,19 +81,159 @@ class SimulationNetwork:
|
||||
|
||||
def connect(
|
||||
self,
|
||||
source_component: str,
|
||||
source_port: str,
|
||||
target_component: str,
|
||||
target_port: str,
|
||||
) -> None:
|
||||
self.connections.append(
|
||||
Connection(
|
||||
source_component=source_component,
|
||||
source_port=source_port,
|
||||
target_component=target_component,
|
||||
target_port=target_port,
|
||||
endpoint_a_component: str,
|
||||
endpoint_a_port: str,
|
||||
endpoint_b_component: str,
|
||||
endpoint_b_port: str,
|
||||
*,
|
||||
connection_id: str | None = None,
|
||||
) -> Connection:
|
||||
endpoint_a = Endpoint(endpoint_a_component, endpoint_a_port)
|
||||
endpoint_b = Endpoint(endpoint_b_component, endpoint_b_port)
|
||||
if endpoint_a == endpoint_b:
|
||||
raise ValueError(f"Cannot connect endpoint {endpoint_a} to itself.")
|
||||
|
||||
first_port = self._port_for(endpoint_a)
|
||||
second_port = self._port_for(endpoint_b)
|
||||
first_definition = first_port.definition
|
||||
second_definition = second_port.definition
|
||||
if first_definition is None or second_definition is None:
|
||||
raise ValueError("Connected ports must expose interface definitions.")
|
||||
if first_definition.kind != second_definition.kind:
|
||||
raise ValueError(f"Connection mixes physical and signal ports: {endpoint_a}, {endpoint_b}.")
|
||||
if first_definition.domain != second_definition.domain:
|
||||
raise ValueError(f"Connection domains do not match: {endpoint_a}, {endpoint_b}.")
|
||||
if first_definition.variables != second_definition.variables:
|
||||
raise ValueError(
|
||||
f"Connection variable contracts do not match: {endpoint_a}, {endpoint_b}."
|
||||
)
|
||||
if first_definition.kind == "signal" and {
|
||||
first_definition.nominal_role,
|
||||
second_definition.nominal_role,
|
||||
} != {"input", "output"}:
|
||||
raise ValueError("A signal connection must contain one output and one input.")
|
||||
|
||||
occupied_endpoints = {
|
||||
endpoint
|
||||
for item in self.connections
|
||||
for endpoint in item.endpoints
|
||||
}
|
||||
if first_definition.kind == "physical":
|
||||
occupied = [
|
||||
str(endpoint)
|
||||
for endpoint in (endpoint_a, endpoint_b)
|
||||
if endpoint in occupied_endpoints
|
||||
]
|
||||
if occupied:
|
||||
raise ValueError(
|
||||
"Physical ports accept one connection; already connected: "
|
||||
+ ", ".join(occupied)
|
||||
+ ". Use a junction component for branching."
|
||||
)
|
||||
|
||||
if first_definition.kind == "physical" and endpoint_b.key < endpoint_a.key:
|
||||
endpoint_a, endpoint_b = endpoint_b, endpoint_a
|
||||
|
||||
connection = Connection(
|
||||
id=connection_id or f"connection_{len(self.connections) + 1}",
|
||||
kind=first_definition.kind,
|
||||
domain=first_definition.domain,
|
||||
endpoint_a=endpoint_a,
|
||||
endpoint_b=endpoint_b,
|
||||
)
|
||||
if any(item.undirected_key == connection.undirected_key for item in self.connections):
|
||||
raise ValueError(f"Duplicate connection between {endpoint_a} and {endpoint_b}.")
|
||||
if any(item.id == connection.id for item in self.connections):
|
||||
raise ValueError(f"Duplicate connection id: {connection.id}.")
|
||||
self.connections.append(connection)
|
||||
return connection
|
||||
|
||||
def _port_for(self, endpoint: Endpoint) -> PortState:
|
||||
try:
|
||||
component = self.components[endpoint.component]
|
||||
except KeyError as exc:
|
||||
raise ValueError(f"Unknown component: {endpoint.component}.") from exc
|
||||
return component.get_port(endpoint.port)
|
||||
|
||||
def connection_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
"""Evaluate connector equations that have a direct scalar residual.
|
||||
|
||||
Stream variables are resolved by the stream-mixing layer and therefore do
|
||||
not incorrectly appear here as an equality between outflow properties.
|
||||
"""
|
||||
|
||||
residuals: list[EquationResidual] = []
|
||||
for connection in self.connections:
|
||||
if connection.kind != "physical":
|
||||
continue
|
||||
|
||||
first_port = self._port_for(connection.endpoint_a)
|
||||
second_port = self._port_for(connection.endpoint_b)
|
||||
definition = first_port.definition
|
||||
if definition is None:
|
||||
raise ValueError(
|
||||
f"Connected port {connection.endpoint_a} has no interface definition."
|
||||
)
|
||||
|
||||
for variable in definition.variables:
|
||||
if variable.connection_rule == "equal":
|
||||
value = float(getattr(first_port, variable.name)) - float(
|
||||
getattr(second_port, variable.name)
|
||||
)
|
||||
elif variable.connection_rule == "sumToZero":
|
||||
value = float(getattr(first_port, variable.name)) + float(
|
||||
getattr(second_port, variable.name)
|
||||
)
|
||||
else:
|
||||
continue
|
||||
residuals.append(
|
||||
EquationResidual(
|
||||
id=f"{connection.id}:{variable.name}",
|
||||
owner="connection",
|
||||
owner_id=connection.id,
|
||||
relation=variable.connection_rule,
|
||||
variables=(
|
||||
f"{connection.endpoint_a}.{variable.name}",
|
||||
f"{connection.endpoint_b}.{variable.name}",
|
||||
),
|
||||
role=variable.role,
|
||||
value=value,
|
||||
)
|
||||
)
|
||||
return tuple(residuals)
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
"""Evaluate the complete algebraic pressure-flow equation subsystem."""
|
||||
|
||||
component_residuals = tuple(
|
||||
residual
|
||||
for component in self.components.values()
|
||||
for residual in component.pressure_flow_equation_residuals()
|
||||
)
|
||||
return component_residuals + self.connection_equation_residuals()
|
||||
|
||||
def pressure_flow_unknowns(self) -> tuple[str, ...]:
|
||||
return tuple(
|
||||
f"{component.name}.{definition.name}.{variable.name}"
|
||||
for component in self.components.values()
|
||||
for definition in component.port_definitions
|
||||
if definition.kind == "physical"
|
||||
for variable in definition.variables
|
||||
if variable.role in {"effort", "flow"}
|
||||
)
|
||||
|
||||
def pressure_flow_structure_dict(self) -> dict[str, object]:
|
||||
unknowns = self.pressure_flow_unknowns()
|
||||
equations = self.pressure_flow_equation_residuals()
|
||||
return {
|
||||
"unknownCount": len(unknowns),
|
||||
"equationCount": len(equations),
|
||||
"isSquare": len(unknowns) == len(equations),
|
||||
"unknowns": list(unknowns),
|
||||
"equations": [
|
||||
equation.as_definition_dict() for equation in equations
|
||||
],
|
||||
}
|
||||
|
||||
def dynamic_components(self) -> list[DynamicComponent]:
|
||||
return [
|
||||
@@ -70,9 +263,40 @@ class SimulationNetwork:
|
||||
lines.append(f" - {name}: {component.__class__.__name__}")
|
||||
lines.append("Connections:")
|
||||
for conn in self.connections:
|
||||
connector = "<->" if conn.kind == "physical" else "->"
|
||||
lines.append(
|
||||
f" - {conn.source_component}.{conn.source_port}"
|
||||
f" -> {conn.target_component}.{conn.target_port}"
|
||||
f" - {conn.endpoint_a} {connector} {conn.endpoint_b}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
def as_interface_dict(self) -> dict[str, object]:
|
||||
connected_endpoints = {
|
||||
endpoint.key
|
||||
for connection in self.connections
|
||||
for endpoint in connection.endpoints
|
||||
}
|
||||
return {
|
||||
"name": self.name,
|
||||
"components": [
|
||||
{
|
||||
"id": component.name,
|
||||
"type": component.model_type,
|
||||
"ports": [
|
||||
definition.as_interface_dict()
|
||||
for definition in component.port_definitions
|
||||
],
|
||||
}
|
||||
for component in self.components.values()
|
||||
],
|
||||
"connections": [
|
||||
connection.as_interface_dict() for connection in self.connections
|
||||
],
|
||||
"pressureFlowSystem": self.pressure_flow_structure_dict(),
|
||||
"unconnectedPorts": [
|
||||
{"component": component.name, "port": definition.name}
|
||||
for component in self.components.values()
|
||||
for definition in component.port_definitions
|
||||
if (component.name, definition.name) not in connected_endpoints
|
||||
],
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user