公开PNRP17并接通实时气动机械耦合
This commit is contained in:
1 parent
503394f3ed
commit
1f4027573f
18 files changed
+693
-29
No files matched your search
@@ -9,7 +9,7 @@ from app.simulation.core.catalog import (
|
||||
LIBRARY = ComponentLibrarySpec(
|
||||
id="amesim",
|
||||
label="AMESim 组件库",
|
||||
version="0.2.0",
|
||||
version="0.3.0",
|
||||
source_package="app.simulation.components.amesim",
|
||||
temporary=True,
|
||||
order=200,
|
||||
@@ -33,6 +33,7 @@ LIBRARY = ComponentLibrarySpec(
|
||||
"app.simulation.components.amesim.mechanical.translational:AmesimMecmas21",
|
||||
"app.simulation.components.amesim.mechanical.translational:AmesimLstp00a",
|
||||
"app.simulation.components.amesim.mechanical.translational:AmesimLmechn1",
|
||||
"app.simulation.components.amesim.mechanical.pistons:AmesimPnrp17",
|
||||
"app.simulation.components.amesim.storage.chambers:AmesimPnch023",
|
||||
"app.simulation.components.amesim.storage.chambers:AmesimPnch012",
|
||||
"app.simulation.components.amesim.flow.orifices:AmesimPnor001",
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from math import pi
|
||||
|
||||
from app.simulation.components.amesim.gases import (
|
||||
AMESIM_GAS_INDEX_PARAMETER,
|
||||
normalize_amesim_gas_index,
|
||||
)
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.metadata import ParameterDefinition, ResultVariableDefinition
|
||||
from app.simulation.core.medium import GasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
|
||||
|
||||
AMESIM_REFERENCE_PRESSURE_PA = 101300.0
|
||||
|
||||
|
||||
class AmesimPnrp17(AlgebraicComponent):
|
||||
"""AMESim PNRP17 pneumatic piston with two mechanical faces.
|
||||
|
||||
Mechanical ports 2/5 share the piston-side motion and ports 3/4 share the
|
||||
cylinder-side motion. The pneumatic port contributes its swept volume and
|
||||
volume rate to the connected variable-volume chamber.
|
||||
"""
|
||||
|
||||
MODEL_TYPE = "amesim_pnrp17"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
||||
PortDefinition.mechanical_translational("port_2"),
|
||||
PortDefinition.mechanical_translational("port_3"),
|
||||
PortDefinition.mechanical_translational("port_4"),
|
||||
PortDefinition.mechanical_translational("port_5"),
|
||||
)
|
||||
PARAMETERS = (
|
||||
AMESIM_GAS_INDEX_PARAMETER,
|
||||
ParameterDefinition(
|
||||
"dp",
|
||||
0.2,
|
||||
label="活塞直径",
|
||||
quantity="length",
|
||||
unit="m",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
description="活塞外径;与活塞杆直径共同确定有效受压面积。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"dr",
|
||||
0.001,
|
||||
label="活塞杆直径",
|
||||
quantity="length",
|
||||
unit="m",
|
||||
minimum=0.0,
|
||||
description="穿过气室一侧的活塞杆直径,必须不大于活塞直径。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"x0",
|
||||
0.0,
|
||||
label="初始腔长",
|
||||
quantity="length",
|
||||
unit="m",
|
||||
description="机械端位移均为零时的气动腔长度。",
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition("volume", "扫掠容积", "volume", "m3", "derived", 10),
|
||||
ResultVariableDefinition(
|
||||
"volume_flow",
|
||||
"扫掠容积变化率",
|
||||
"volume_flow",
|
||||
"m3/s",
|
||||
"derived",
|
||||
20,
|
||||
),
|
||||
ResultVariableDefinition("length", "气动腔长度", "length", "m", "derived", 30),
|
||||
ResultVariableDefinition(
|
||||
"pressure_force",
|
||||
"气压力",
|
||||
"force",
|
||||
"N",
|
||||
"derived",
|
||||
40,
|
||||
),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PNRP17 气动活塞",
|
||||
library_id="amesim",
|
||||
category_id="mechanical",
|
||||
symbol="amesim_pnrp17",
|
||||
ports=(
|
||||
PortDisplaySpec("port_1", "left", order=10),
|
||||
PortDisplaySpec("port_3", "left", order=20),
|
||||
PortDisplaySpec("port_4", "left", order=30),
|
||||
PortDisplaySpec("port_2", "right", order=40),
|
||||
PortDisplaySpec("port_5", "right", order=50),
|
||||
),
|
||||
order=60,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
*,
|
||||
gi: float = 0.0,
|
||||
dp: float = 0.2,
|
||||
dr: float = 0.001,
|
||||
x0: float = 0.0,
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({"gi": gi, "dp": dp, "dr": dr, "x0": x0})
|
||||
self.medium = medium
|
||||
self.gi = normalize_amesim_gas_index(gi)
|
||||
self.dp = float(dp)
|
||||
self.dr = float(dr)
|
||||
self.x0 = float(x0)
|
||||
if self.dr > self.dp:
|
||||
raise ValueError("PNRP17 rod diameter dr must not exceed piston diameter dp.")
|
||||
for definition in self.PORTS:
|
||||
port = self.register_declared_port(definition.name)
|
||||
setattr(self, definition.name, port)
|
||||
self.port_1.h_outflow = medium.specific_enthalpy(medium.T_ref)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimPnrp17":
|
||||
return cls(name=name, medium=medium, **dict(parameters))
|
||||
|
||||
@property
|
||||
def effective_area(self) -> float:
|
||||
return pi * (self.dp * self.dp - self.dr * self.dr) / 4.0
|
||||
|
||||
@property
|
||||
def chamber_length(self) -> float:
|
||||
return self.x0 + self.port_5.x - self.port_4.x
|
||||
|
||||
@property
|
||||
def chamber_volume(self) -> float:
|
||||
return self.effective_area * self.chamber_length
|
||||
|
||||
@property
|
||||
def chamber_volume_flow(self) -> float:
|
||||
return self.effective_area * (self.port_5.v - self.port_4.v)
|
||||
|
||||
@property
|
||||
def pressure_force(self) -> float:
|
||||
return (self.port_1.p - AMESIM_REFERENCE_PRESSURE_PA) * self.effective_area
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
effort_pairs = (("port_2", "port_5"), ("port_3", "port_4"))
|
||||
residuals: list[EquationResidual] = [
|
||||
EquationResidual(
|
||||
id=f"{self.name}:pneumatic_zero_mass_flow",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.port_1.m_flow",),
|
||||
role="flow",
|
||||
value=self.port_1.m_flow,
|
||||
)
|
||||
]
|
||||
for first_name, second_name in effort_pairs:
|
||||
first = self.get_port(first_name)
|
||||
second = self.get_port(second_name)
|
||||
for variable in ("x", "v"):
|
||||
residuals.append(
|
||||
EquationResidual(
|
||||
id=f"{self.name}:{first_name}_{second_name}_{variable}_equal",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="equal",
|
||||
variables=(
|
||||
f"{self.name}.{first_name}.{variable}",
|
||||
f"{self.name}.{second_name}.{variable}",
|
||||
),
|
||||
role="effort",
|
||||
value=getattr(first, variable) - getattr(second, variable),
|
||||
)
|
||||
)
|
||||
force = self.pressure_force
|
||||
residuals.extend(
|
||||
(
|
||||
EquationResidual(
|
||||
id=f"{self.name}:piston_side_force_balance",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.port_2.f", f"{self.name}.port_5.f", f"{self.name}.port_1.p"),
|
||||
role="flow",
|
||||
value=self.port_2.f + self.port_5.f + force,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:cylinder_side_force_balance",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.port_3.f", f"{self.name}.port_4.f", f"{self.name}.port_1.p"),
|
||||
role="flow",
|
||||
value=self.port_3.f + self.port_4.f - force,
|
||||
),
|
||||
)
|
||||
)
|
||||
return tuple(residuals)
|
||||
|
||||
def pneumatic_volume_outputs(self) -> Mapping[str, tuple[float, float]]:
|
||||
return {"port_1": (self.chamber_volume, self.chamber_volume_flow)}
|
||||
|
||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||
self.port_1.h_outflow = connected_h.get(
|
||||
"port_1",
|
||||
self.medium.specific_enthalpy(self.medium.T_ref),
|
||||
)
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
return {
|
||||
"volume": self.chamber_volume,
|
||||
"volume_flow": self.chamber_volume_flow,
|
||||
"length": self.chamber_length,
|
||||
"pressure_force": self.pressure_force,
|
||||
}
|
||||
@@ -249,10 +249,9 @@ class AmesimPnch012(ThermodynamicVolumeComponent):
|
||||
"""AMESim PNCH012 variable-volume pneumatic chamber.
|
||||
|
||||
AMESim supplies four external volume and volume-rate inputs through the
|
||||
chamber ports. The current public System XML contract has pneumatic ports
|
||||
only, so this first public model exposes those external volume inputs as SI
|
||||
parameters. This represents fixed or prescribed-volume PNCH012 cases and is
|
||||
not yet the full mechanical-coupled submodel.
|
||||
chamber ports. Fixed/prescribed contributions remain available as SI
|
||||
parameters, while connected moving-boundary components can now add live
|
||||
volume and volume-rate values through the pneumatic connector contract.
|
||||
"""
|
||||
|
||||
MODEL_TYPE = "amesim_pnch012"
|
||||
@@ -433,14 +432,29 @@ class AmesimPnch012(ThermodynamicVolumeComponent):
|
||||
) -> "AmesimPnch012":
|
||||
return cls(name=name, medium=medium, **dict(parameters))
|
||||
|
||||
def connected_external_volume(self) -> float:
|
||||
return sum(
|
||||
getattr(getattr(self, port_name, None), "volume", 0.0)
|
||||
for port_name in self.external_volumes
|
||||
)
|
||||
|
||||
def connected_external_volume_rate(self) -> float:
|
||||
return sum(
|
||||
getattr(getattr(self, port_name, None), "volume_flow", 0.0)
|
||||
for port_name in self.external_volume_rates
|
||||
)
|
||||
|
||||
def total_volume(self) -> float:
|
||||
minimum_volume = self.cvol0 / 100.0
|
||||
return max(self.cvol0 + sum(self.external_volumes.values()), minimum_volume)
|
||||
return max(
|
||||
self.cvol0 + sum(self.external_volumes.values()) + self.connected_external_volume(),
|
||||
minimum_volume,
|
||||
)
|
||||
|
||||
def total_volume_rate(self) -> float:
|
||||
if self.total_volume() <= self.cvol0 / 100.0:
|
||||
return 0.0
|
||||
return sum(self.external_volume_rates.values())
|
||||
return sum(self.external_volume_rates.values()) + self.connected_external_volume_rate()
|
||||
|
||||
def get_state_vector(self) -> list[float]:
|
||||
return self.state.as_vector()
|
||||
|
||||
@@ -204,6 +204,16 @@ class Component(ABC):
|
||||
|
||||
return None
|
||||
|
||||
def pneumatic_volume_outputs(self) -> Mapping[str, tuple[float, float]]:
|
||||
"""Return directed ``volume``/``volume_flow`` values by pneumatic port.
|
||||
|
||||
Most pneumatic components contribute no external chamber volume. Moving
|
||||
boundaries such as PNRP17 override this hook; the network resolver then
|
||||
propagates the pair to the component connected at the same physical port.
|
||||
"""
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
class DynamicComponent(Component):
|
||||
state_size = 2
|
||||
|
||||
@@ -87,6 +87,26 @@ class PortDefinition:
|
||||
unit="J/kg",
|
||||
order=30,
|
||||
),
|
||||
PortVariableDefinition(
|
||||
"volume",
|
||||
"signal",
|
||||
"directed",
|
||||
label="外部容积",
|
||||
quantity="volume",
|
||||
unit="m3",
|
||||
result_visible=False,
|
||||
order=40,
|
||||
),
|
||||
PortVariableDefinition(
|
||||
"volume_flow",
|
||||
"signal",
|
||||
"directed",
|
||||
label="外部容积变化率",
|
||||
quantity="volume_flow",
|
||||
unit="m3/s",
|
||||
result_visible=False,
|
||||
order=50,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -178,6 +198,8 @@ class PortState:
|
||||
p: float = 0.0
|
||||
m_flow: float = 0.0
|
||||
h_outflow: float = 0.0
|
||||
volume: float = 0.0
|
||||
volume_flow: float = 0.0
|
||||
signal: float = 0.0
|
||||
x: float = 0.0
|
||||
v: float = 0.0
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from math import isfinite
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
|
||||
class PneumaticVolumeResolver:
|
||||
"""Propagate AMESim pneumatic external-volume connector variables."""
|
||||
|
||||
def __init__(self, network: SimulationNetwork) -> None:
|
||||
self.network = network
|
||||
self._connected_endpoint = self._build_connection_map()
|
||||
self.last_diagnostics: PneumaticVolumeDiagnostics | None = None
|
||||
|
||||
def _build_connection_map(self) -> dict[Endpoint, Endpoint]:
|
||||
result: dict[Endpoint, Endpoint] = {}
|
||||
for connection in self.network.connections:
|
||||
if connection.kind != "physical" or connection.domain != "pneumatic":
|
||||
continue
|
||||
first, second = connection.endpoints
|
||||
result[first] = second
|
||||
result[second] = first
|
||||
return result
|
||||
|
||||
def solve(self) -> PneumaticVolumeDiagnostics:
|
||||
for component in self.network.components.values():
|
||||
for definition in component.port_definitions:
|
||||
if definition.kind == "physical" and definition.domain == "pneumatic":
|
||||
port = component.get_port(definition.name)
|
||||
port.volume = 0.0
|
||||
port.volume_flow = 0.0
|
||||
|
||||
outputs: dict[Endpoint, tuple[float, float]] = {}
|
||||
for component in self.network.components.values():
|
||||
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():
|
||||
connected = self._connected_endpoint.get(endpoint)
|
||||
if connected is None:
|
||||
continue
|
||||
if connected in outputs:
|
||||
raise ValueError(
|
||||
"A pneumatic connection cannot contain two external-volume "
|
||||
f"sources: {endpoint} and {connected}."
|
||||
)
|
||||
connected_port = self.network.components[connected.component].get_port(
|
||||
connected.port
|
||||
)
|
||||
connected_port.volume, 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
|
||||
@@ -8,6 +8,7 @@ from typing import Literal
|
||||
from app.simulation.core.base import DynamicComponent
|
||||
from app.simulation.core.metadata import ResultVariableMetadata
|
||||
from app.simulation.solvers.algebraic import PressureFlowSolver
|
||||
from app.simulation.solvers.pneumatic_volume import PneumaticVolumeResolver
|
||||
from app.simulation.solvers.solver import ODESolution, SolveIVPConfig, integrate_ode
|
||||
from app.simulation.solvers.signal import SignalResolver
|
||||
from app.simulation.solvers.stream import StreamResolver
|
||||
@@ -233,6 +234,7 @@ class GenericFluidSystem:
|
||||
self.network = network
|
||||
self.dynamic_components = network.dynamic_components()
|
||||
self.pressure_flow_solver = PressureFlowSolver(network)
|
||||
self.pneumatic_volume_resolver = PneumaticVolumeResolver(network)
|
||||
self.signal_resolver = SignalResolver(network)
|
||||
self.stream_resolver = StreamResolver(network)
|
||||
self.algebraic_solve_count = 0
|
||||
@@ -240,6 +242,7 @@ class GenericFluidSystem:
|
||||
self.max_algebraic_evaluations = 0
|
||||
self.max_stream_iterations = 0
|
||||
self.signal_propagation_count = 0
|
||||
self.pneumatic_volume_propagation_count = 0
|
||||
|
||||
def initial_state_vector(self) -> list[float]:
|
||||
return self.network.initial_state_vector()
|
||||
@@ -253,8 +256,14 @@ class GenericFluidSystem:
|
||||
for component in self.dynamic_components:
|
||||
component.refresh_thermodynamic_ports()
|
||||
algebraic = self.pressure_flow_solver.solve()
|
||||
pneumatic_volume = self.pneumatic_volume_resolver.solve()
|
||||
self.pneumatic_volume_propagation_count += pneumatic_volume.propagated
|
||||
if pneumatic_volume.propagated:
|
||||
for component in self.dynamic_components:
|
||||
component.refresh_thermodynamic_ports()
|
||||
algebraic = self.pressure_flow_solver.solve()
|
||||
stream, connected_h = self.stream_resolver.solve()
|
||||
self.algebraic_solve_count += 1
|
||||
self.algebraic_solve_count += 1 + int(bool(pneumatic_volume.propagated))
|
||||
self.max_algebraic_residual = max(
|
||||
self.max_algebraic_residual,
|
||||
algebraic.max_scaled_residual,
|
||||
@@ -440,6 +449,14 @@ class GenericFluidSystem:
|
||||
else None
|
||||
),
|
||||
},
|
||||
"pneumaticVolume": {
|
||||
"propagations": self.pneumatic_volume_propagation_count,
|
||||
"last": (
|
||||
self.pneumatic_volume_resolver.last_diagnostics.as_dict()
|
||||
if self.pneumatic_volume_resolver.last_diagnostics is not None
|
||||
else None
|
||||
),
|
||||
},
|
||||
"stateCount": len(initial_state),
|
||||
"sampleCount": len(series["time"]),
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user