122 lines
4.7 KiB
Python
122 lines
4.7 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from math import isfinite
|
|
|
|
from app.simulation.core.base import Component
|
|
from app.simulation.core.ports import PortState
|
|
from app.simulation.performance import profile_phase
|
|
from app.simulation.systems.network import Endpoint, SimulationNetwork
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PneumaticVolumeDiagnostics:
|
|
propagated: int
|
|
output_ports: tuple[str, ...]
|
|
|
|
def as_dict(self) -> dict[str, object]:
|
|
return {
|
|
"propagated": self.propagated,
|
|
"outputPorts": list(self.output_ports),
|
|
}
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class _PneumaticVolumeConnectionBinding:
|
|
connected_endpoint: Endpoint
|
|
connected_port: PortState
|
|
|
|
|
|
class PneumaticVolumeResolver:
|
|
"""Propagate AMESim pneumatic external-volume connector variables."""
|
|
|
|
def __init__(self, network: SimulationNetwork) -> None:
|
|
self.network = network
|
|
self._pneumatic_ports = tuple(
|
|
component.get_port(definition.name)
|
|
for component in network.components.values()
|
|
for definition in component.active_port_definitions
|
|
if definition.kind == "physical" and definition.domain == "pneumatic"
|
|
)
|
|
self._output_components = tuple(
|
|
component
|
|
for component in network.components.values()
|
|
if type(component).pneumatic_volume_outputs
|
|
is not Component.pneumatic_volume_outputs
|
|
)
|
|
self._connected_endpoint = self._build_connection_map()
|
|
self.last_diagnostics: PneumaticVolumeDiagnostics | None = None
|
|
|
|
def _build_connection_map(
|
|
self,
|
|
) -> dict[Endpoint, _PneumaticVolumeConnectionBinding]:
|
|
result: dict[Endpoint, _PneumaticVolumeConnectionBinding] = {}
|
|
for connection in self.network.connections:
|
|
if connection.kind != "physical" or connection.domain != "pneumatic":
|
|
continue
|
|
first, second = connection.endpoints
|
|
result[first] = _PneumaticVolumeConnectionBinding(
|
|
connected_endpoint=second,
|
|
connected_port=self.network.components[second.component].get_port(
|
|
second.port
|
|
),
|
|
)
|
|
result[second] = _PneumaticVolumeConnectionBinding(
|
|
connected_endpoint=first,
|
|
connected_port=self.network.components[first.component].get_port(
|
|
first.port
|
|
),
|
|
)
|
|
return result
|
|
|
|
@profile_phase("simulation.pneumatic_volume", minimum_mode="audit")
|
|
def solve(self) -> PneumaticVolumeDiagnostics:
|
|
for port in self._pneumatic_ports:
|
|
port.volume = 0.0
|
|
port.volume_flow = 0.0
|
|
|
|
outputs: dict[Endpoint, tuple[float, float]] = {}
|
|
for component in self._output_components:
|
|
for port_name, raw_values in component.pneumatic_volume_outputs().items():
|
|
port = component.get_port(port_name)
|
|
definition = port.definition
|
|
if (
|
|
definition is None
|
|
or definition.kind != "physical"
|
|
or definition.domain != "pneumatic"
|
|
):
|
|
raise ValueError(
|
|
f"Component {component.name} declares pneumatic volume output "
|
|
f"on non-pneumatic port {port_name}."
|
|
)
|
|
volume, volume_flow = (float(raw_values[0]), float(raw_values[1]))
|
|
if not isfinite(volume) or not isfinite(volume_flow):
|
|
raise ValueError(
|
|
f"Component {component.name}.{port_name} produced a non-finite "
|
|
"pneumatic volume value."
|
|
)
|
|
endpoint = Endpoint(component.name, port_name)
|
|
outputs[endpoint] = (volume, volume_flow)
|
|
port.volume = volume
|
|
port.volume_flow = volume_flow
|
|
|
|
propagated = 0
|
|
for endpoint, values in outputs.items():
|
|
binding = self._connected_endpoint.get(endpoint)
|
|
if binding is None:
|
|
continue
|
|
if binding.connected_endpoint in outputs:
|
|
raise ValueError(
|
|
"A pneumatic connection cannot contain two external-volume "
|
|
f"sources: {endpoint} and {binding.connected_endpoint}."
|
|
)
|
|
binding.connected_port.volume, binding.connected_port.volume_flow = values
|
|
propagated += 1
|
|
|
|
diagnostics = PneumaticVolumeDiagnostics(
|
|
propagated=propagated,
|
|
output_ports=tuple(sorted(str(endpoint) for endpoint in outputs)),
|
|
)
|
|
self.last_diagnostics = diagnostics
|
|
return diagnostics
|