公开更多 AMESim 组件并接入信号机械闭环
This commit is contained in:
1 parent
db4bdb4b70
commit
3f51a124d6
28 files changed
+3397
-58
No files matched your search
@@ -608,3 +608,94 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent):
|
||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||
self.port_2.h_outflow = connected_h["port_3"]
|
||||
self.port_3.h_outflow = connected_h["port_2"]
|
||||
|
||||
|
||||
class AmesimPnvo001SignalOpening(AmesimPnvo001FixedOpening):
|
||||
"""AMESim PNVO001 signal-controlled pneumatic orifice."""
|
||||
|
||||
MODEL_TYPE = "amesim_pnvo001"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.signal("res", nominal_role="input"),
|
||||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_3", nominal_role="bidirectional"),
|
||||
)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition("cq", 0.72, label="流量系数 Cq", quantity="dimensionless", unit="", minimum=1.0e-10, maximum=1.0),
|
||||
ParameterDefinition("area0", 5.0e-6, label="最大孔口面积", quantity="area", unit="m2", minimum=0.0, maximum=1.0),
|
||||
ParameterDefinition("Cv", 0.5, label="最大流量系数 Cv", quantity="dimensionless", unit="", minimum=0.0),
|
||||
ParameterDefinition("Kv", 0.4, label="最大流量系数 Kv", quantity="dimensionless", unit="", minimum=0.0),
|
||||
ParameterDefinition("gi", 1.0, label="气体类型索引", quantity="dimensionless", unit="", minimum=1.0, maximum=99.0),
|
||||
ParameterDefinition("flowset", 1.0, label="流量系数设置", quantity="dimensionless", unit="", minimum=1.0, maximum=3.0),
|
||||
ParameterDefinition("opening0", 1.0, label="初始开度", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0),
|
||||
)
|
||||
RESULT_VARIABLES = AmesimPnvo001FixedOpening.RESULT_VARIABLES
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PNVO001 信号开度气动孔口",
|
||||
library_id="amesim",
|
||||
category_id="flow",
|
||||
symbol="orifice",
|
||||
ports=(
|
||||
PortDisplaySpec("res", "left", order=5),
|
||||
PortDisplaySpec("port_2", "left", order=10),
|
||||
PortDisplaySpec("port_3", "right", order=20),
|
||||
),
|
||||
order=35,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
*,
|
||||
cq: float = 0.72,
|
||||
area0: float = 5.0e-6,
|
||||
Cv: float = 0.5,
|
||||
Kv: float = 0.4,
|
||||
gi: float = 1.0,
|
||||
flowset: float = 1.0,
|
||||
opening0: float = 1.0,
|
||||
) -> None:
|
||||
AlgebraicComponent.__init__(self, name=name)
|
||||
self.set_parameter_values(
|
||||
{
|
||||
"cq": cq,
|
||||
"area0": area0,
|
||||
"Cv": Cv,
|
||||
"Kv": Kv,
|
||||
"gi": gi,
|
||||
"flowset": flowset,
|
||||
"opening0": opening0,
|
||||
}
|
||||
)
|
||||
self.medium = medium
|
||||
self.cq = float(cq)
|
||||
self.area0 = float(area0)
|
||||
self.Cv = float(Cv)
|
||||
self.Kv = float(Kv)
|
||||
self.gi = self._integer_parameter("gi", gi)
|
||||
self.flowset = self._integer_parameter("flowset", flowset)
|
||||
if self.flowset not in {1, 2, 3}:
|
||||
raise ValueError("PNVO001 signal-opening flowset must be 1, 2, or 3.")
|
||||
self.opening0 = min(1.0, max(0.0, float(opening0)))
|
||||
self.res = self.register_declared_port("res")
|
||||
self.res.signal = self.opening0
|
||||
initial_h = medium.specific_enthalpy(medium.T_ref)
|
||||
self.port_2 = self.register_declared_port("port_2")
|
||||
self.port_2.h_outflow = initial_h
|
||||
self.port_3 = self.register_declared_port("port_3")
|
||||
self.port_3.h_outflow = initial_h
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimPnvo001SignalOpening":
|
||||
return cls(name=name, medium=medium, **dict(parameters))
|
||||
|
||||
@property
|
||||
def opening(self) -> float:
|
||||
return min(1.0, max(0.0, self.res.signal))
|
||||
@@ -3,15 +3,17 @@ from __future__ import annotations
|
||||
from collections.abc import Mapping
|
||||
from math import isclose, log10, pi, sqrt
|
||||
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.base import AlgebraicComponent, DynamicComponent, ThermodynamicVolumeComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.metadata import (
|
||||
ParameterDefinition,
|
||||
ResultVariableDefinition,
|
||||
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
|
||||
)
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.medium import IdealGasMedium, ThermodynamicProperties
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
from app.simulation.core.state import VolumeState
|
||||
|
||||
|
||||
class AmesimPnl00r(AlgebraicComponent):
|
||||
@@ -308,3 +310,887 @@ class AmesimPnl00r(AlgebraicComponent):
|
||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||
self.port_1.h_outflow = connected_h["port_2"]
|
||||
self.port_2.h_outflow = connected_h["port_1"]
|
||||
|
||||
|
||||
class AmesimPnl0001(ThermodynamicVolumeComponent):
|
||||
"""AMESim PNL0001 C-R pneumatic pipe with compressibility and friction."""
|
||||
|
||||
MODEL_TYPE = "amesim_pnl0001"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||||
)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition(
|
||||
"diam",
|
||||
0.01,
|
||||
label="管径",
|
||||
quantity="length",
|
||||
unit="m",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"le",
|
||||
1.0,
|
||||
label="管长",
|
||||
quantity="length",
|
||||
unit="m",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"rr",
|
||||
1.0e-5,
|
||||
label="相对粗糙度",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=0.0,
|
||||
maximum=0.1,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"k",
|
||||
1.35,
|
||||
label="多方指数",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
maximum=2.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"kth",
|
||||
0.0,
|
||||
label="换热系数",
|
||||
quantity="heat_transfer_coefficient",
|
||||
unit="W/(m2*K)",
|
||||
minimum=0.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"extemp",
|
||||
293.15,
|
||||
label="外部温度",
|
||||
quantity="temperature",
|
||||
unit="K",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"gi",
|
||||
1.0,
|
||||
label="气体类型索引",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=1.0,
|
||||
maximum=99.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"mode",
|
||||
2.0,
|
||||
label="热模型",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=1.0,
|
||||
maximum=2.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"p0",
|
||||
100000.0,
|
||||
label="初始压力",
|
||||
quantity="pressure",
|
||||
unit="Pa",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"T0",
|
||||
293.15,
|
||||
label="初始温度",
|
||||
quantity="temperature",
|
||||
unit="K",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES + (
|
||||
ResultVariableDefinition(
|
||||
"re",
|
||||
label="Reynolds 数",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
category="derived",
|
||||
order=100,
|
||||
),
|
||||
ResultVariableDefinition(
|
||||
"cm",
|
||||
label="质量流量参数",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
category="derived",
|
||||
order=110,
|
||||
),
|
||||
ResultVariableDefinition(
|
||||
"v",
|
||||
label="平均气体速度",
|
||||
quantity="velocity",
|
||||
unit="m/s",
|
||||
category="derived",
|
||||
order=120,
|
||||
),
|
||||
ResultVariableDefinition(
|
||||
"ff",
|
||||
label="摩擦因子",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
category="derived",
|
||||
order=130,
|
||||
),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PNL0001 C-R 动态管路",
|
||||
library_id="amesim",
|
||||
category_id="flow",
|
||||
symbol="pipe",
|
||||
ports=(
|
||||
PortDisplaySpec("port_1", "left", order=10),
|
||||
PortDisplaySpec("port_2", "right", order=20),
|
||||
),
|
||||
order=30,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
*,
|
||||
diam: float = 0.01,
|
||||
le: float = 1.0,
|
||||
rr: float = 1.0e-5,
|
||||
k: float = 1.35,
|
||||
kth: float = 0.0,
|
||||
extemp: float = 293.15,
|
||||
gi: float = 1.0,
|
||||
mode: float = 2.0,
|
||||
p0: float = 100000.0,
|
||||
T0: float = 293.15,
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values(
|
||||
{
|
||||
"diam": diam,
|
||||
"le": le,
|
||||
"rr": rr,
|
||||
"k": k,
|
||||
"kth": kth,
|
||||
"extemp": extemp,
|
||||
"gi": gi,
|
||||
"mode": mode,
|
||||
"p0": p0,
|
||||
"T0": T0,
|
||||
}
|
||||
)
|
||||
self.medium = medium
|
||||
self.diam = float(diam)
|
||||
self.le = float(le)
|
||||
self.rr = float(rr)
|
||||
self.k = float(k)
|
||||
self.kth = float(kth)
|
||||
self.extemp = float(extemp)
|
||||
self.gi = self._integer_parameter("gi", gi)
|
||||
self.mode = self._integer_parameter("mode", mode)
|
||||
self.p0 = float(p0)
|
||||
self.T0 = float(T0)
|
||||
self.area = pi * self.diam * self.diam / 4.0
|
||||
self.volume = self.area * self.le
|
||||
self.exchange_area = pi * self.diam * self.le
|
||||
m0 = self.p0 * self.volume / (medium.R_gas * self.T0)
|
||||
U0 = m0 * medium.specific_internal_energy(self.T0)
|
||||
self.state = VolumeState(m=m0, U=U0)
|
||||
initial_h = medium.specific_enthalpy(self.T0)
|
||||
self.port_1 = self.register_declared_port("port_1")
|
||||
self.port_1.p = self.p0
|
||||
self.port_1.h_outflow = initial_h
|
||||
self.port_2 = self.register_declared_port("port_2")
|
||||
self.port_2.p = self.p0
|
||||
self.port_2.h_outflow = initial_h
|
||||
|
||||
@staticmethod
|
||||
def _integer_parameter(name: str, value: float) -> int:
|
||||
rounded = round(value)
|
||||
if not isclose(value, rounded, rel_tol=0.0, abs_tol=1.0e-12):
|
||||
raise ValueError(f"PNL0001 parameter {name} must be an integer value.")
|
||||
return int(rounded)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimPnl0001":
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
diam=parameters["diam"],
|
||||
le=parameters["le"],
|
||||
rr=parameters["rr"],
|
||||
k=parameters["k"],
|
||||
kth=parameters["kth"],
|
||||
extemp=parameters["extemp"],
|
||||
gi=parameters["gi"],
|
||||
mode=parameters["mode"],
|
||||
p0=parameters["p0"],
|
||||
T0=parameters["T0"],
|
||||
)
|
||||
|
||||
def get_state_vector(self) -> list[float]:
|
||||
return self.state.as_vector()
|
||||
|
||||
def set_state_vector(self, values: list[float]) -> None:
|
||||
self.state = VolumeState.from_vector(values)
|
||||
|
||||
def properties(self) -> ThermodynamicProperties:
|
||||
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.volume)
|
||||
self.port_1.h_outflow = props.h
|
||||
self.port_2.p = props.p
|
||||
self.port_2.h_outflow = props.h
|
||||
return props
|
||||
|
||||
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
|
||||
return self.properties()
|
||||
|
||||
def thermal_energy_flow_w(self, temperature: float) -> float:
|
||||
if self.mode == 1:
|
||||
return 0.0
|
||||
return self.kth * self.exchange_area * (self.extemp - temperature)
|
||||
|
||||
@staticmethod
|
||||
def _dynamic_viscosity(temperature_k: float) -> float:
|
||||
return AmesimPnl00r._dynamic_viscosity(temperature_k)
|
||||
|
||||
def reynolds_number(self, mass_flow: float, temperature: float) -> float:
|
||||
viscosity = self._dynamic_viscosity(temperature)
|
||||
return 4.0 * abs(mass_flow) / (pi * self.diam * viscosity)
|
||||
|
||||
def friction_factor(self, reynolds_number: float) -> float:
|
||||
return AmesimPnl00r.friction_factor(self, reynolds_number)
|
||||
|
||||
def darcy_pressure_drop(
|
||||
self,
|
||||
mass_flow: float,
|
||||
*,
|
||||
density: float,
|
||||
temperature: float,
|
||||
) -> float:
|
||||
if mass_flow == 0.0:
|
||||
return 0.0
|
||||
reynolds = self.reynolds_number(mass_flow, temperature)
|
||||
friction = self.friction_factor(reynolds)
|
||||
velocity = mass_flow / (density * self.area)
|
||||
magnitude = (
|
||||
friction
|
||||
* (self.le / self.diam)
|
||||
* density
|
||||
* velocity
|
||||
* velocity
|
||||
/ 2.0
|
||||
)
|
||||
return magnitude if mass_flow > 0.0 else -magnitude
|
||||
|
||||
def _mass_flow_for_pressure_drop(
|
||||
self,
|
||||
pressure_drop: float,
|
||||
*,
|
||||
density: float,
|
||||
temperature: float,
|
||||
) -> float:
|
||||
if pressure_drop <= 0.0:
|
||||
return 0.0
|
||||
upper = 1.0e-9
|
||||
while self.darcy_pressure_drop(
|
||||
upper,
|
||||
density=density,
|
||||
temperature=temperature,
|
||||
) < pressure_drop:
|
||||
upper *= 10.0
|
||||
if upper > 1.0e3:
|
||||
raise ValueError("unable to bracket PNL0001 resistance flow")
|
||||
lower = 0.0
|
||||
for _ in range(48):
|
||||
middle = 0.5 * (lower + upper)
|
||||
if self.darcy_pressure_drop(
|
||||
middle,
|
||||
density=density,
|
||||
temperature=temperature,
|
||||
) < pressure_drop:
|
||||
lower = middle
|
||||
else:
|
||||
upper = middle
|
||||
return 0.5 * (lower + upper)
|
||||
|
||||
def mass_flow(self, p_1: float, p_2: float, temperature: float) -> float:
|
||||
if p_1 == p_2:
|
||||
return 0.0
|
||||
pressure_difference = p_1 - p_2
|
||||
upstream_pressure = max(p_1, p_2, 1.0)
|
||||
density = max(self.medium.density(upstream_pressure, temperature), 1.0e-12)
|
||||
magnitude = self._mass_flow_for_pressure_drop(
|
||||
abs(pressure_difference),
|
||||
density=density,
|
||||
temperature=temperature,
|
||||
)
|
||||
return magnitude if pressure_difference > 0.0 else -magnitude
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
props = self.properties()
|
||||
flow = self.mass_flow(self.port_1.p, props.p, props.T)
|
||||
upstream_pressure = max(self.port_1.p, props.p, 1.0)
|
||||
density = max(self.medium.density(upstream_pressure, props.T), 1.0e-12)
|
||||
reynolds = self.reynolds_number(flow, props.T)
|
||||
return {
|
||||
"m": self.state.m,
|
||||
"U": self.state.U,
|
||||
"p": props.p,
|
||||
"T": props.T,
|
||||
"rho": props.rho,
|
||||
"u": props.u,
|
||||
"h": props.h,
|
||||
"re": reynolds,
|
||||
"cm": abs(flow) / max(self.area * upstream_pressure, 1.0e-18),
|
||||
"v": flow / (density * self.area),
|
||||
"ff": self.friction_factor(reynolds),
|
||||
}
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.volume)
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:port_2_pressure_state",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="state",
|
||||
variables=(f"{self.name}.port_2.p", f"{self.name}.state"),
|
||||
role="effort",
|
||||
value=self.port_2.p - props.p,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:port_1_pressure_flow_relation",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(
|
||||
f"{self.name}.port_1.p",
|
||||
f"{self.name}.port_2.p",
|
||||
f"{self.name}.port_1.m_flow",
|
||||
),
|
||||
role="flow",
|
||||
value=self.port_1.m_flow
|
||||
- self.mass_flow(self.port_1.p, props.p, props.T),
|
||||
),
|
||||
)
|
||||
|
||||
def state_derivative_from_ports(
|
||||
self,
|
||||
connected_h: Mapping[str, float],
|
||||
) -> list[float]:
|
||||
props = self.properties()
|
||||
inlet_h_1 = self.connection_inlet_enthalpy(
|
||||
port_m_flow=self.port_1.m_flow,
|
||||
connected_h=connected_h["port_1"],
|
||||
internal_h=props.h,
|
||||
)
|
||||
inlet_h_2 = self.connection_inlet_enthalpy(
|
||||
port_m_flow=self.port_2.m_flow,
|
||||
connected_h=connected_h["port_2"],
|
||||
internal_h=props.h,
|
||||
)
|
||||
derivative = VolumeState(
|
||||
m=self.port_1.m_flow + self.port_2.m_flow,
|
||||
U=(
|
||||
self.port_1.m_flow * inlet_h_1
|
||||
+ self.port_2.m_flow * inlet_h_2
|
||||
+ self.thermal_energy_flow_w(props.T)
|
||||
),
|
||||
)
|
||||
return derivative.as_vector()
|
||||
|
||||
|
||||
class AmesimPnl0002(AmesimPnl0001):
|
||||
"""AMESim PNL0002 R-C-R pneumatic pipe with one center compliance."""
|
||||
|
||||
MODEL_TYPE = "amesim_pnl0002"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||||
)
|
||||
PARAMETERS = AmesimPnl0001.PARAMETERS
|
||||
RESULT_VARIABLES = AmesimPnl0001.RESULT_VARIABLES
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PNL0002 R-C-R 动态管路",
|
||||
library_id="amesim",
|
||||
category_id="flow",
|
||||
symbol="pipe",
|
||||
ports=(
|
||||
PortDisplaySpec("port_1", "left", order=10),
|
||||
PortDisplaySpec("port_2", "right", order=20),
|
||||
),
|
||||
order=40,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimPnl0002":
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
diam=parameters["diam"],
|
||||
le=parameters["le"],
|
||||
rr=parameters["rr"],
|
||||
k=parameters["k"],
|
||||
kth=parameters["kth"],
|
||||
extemp=parameters["extemp"],
|
||||
gi=parameters["gi"],
|
||||
mode=parameters["mode"],
|
||||
p0=parameters["p0"],
|
||||
T0=parameters["T0"],
|
||||
)
|
||||
|
||||
@property
|
||||
def resistance_length(self) -> float:
|
||||
return self.le / 2.0
|
||||
|
||||
def properties(self) -> ThermodynamicProperties:
|
||||
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.volume)
|
||||
self.port_1.h_outflow = props.h
|
||||
self.port_2.h_outflow = props.h
|
||||
return props
|
||||
|
||||
def darcy_pressure_drop(
|
||||
self,
|
||||
mass_flow: float,
|
||||
*,
|
||||
density: float,
|
||||
temperature: float,
|
||||
) -> float:
|
||||
if mass_flow == 0.0:
|
||||
return 0.0
|
||||
reynolds = self.reynolds_number(mass_flow, temperature)
|
||||
friction = self.friction_factor(reynolds)
|
||||
velocity = mass_flow / (density * self.area)
|
||||
magnitude = (
|
||||
friction
|
||||
* (self.resistance_length / self.diam)
|
||||
* density
|
||||
* velocity
|
||||
* velocity
|
||||
/ 2.0
|
||||
)
|
||||
return magnitude if mass_flow > 0.0 else -magnitude
|
||||
|
||||
def port_mass_flow(
|
||||
self,
|
||||
port_pressure: float,
|
||||
center_pressure: float,
|
||||
center_temperature: float,
|
||||
) -> float:
|
||||
return self.mass_flow(port_pressure, center_pressure, center_temperature)
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
props = self.properties()
|
||||
flow_1 = self.port_mass_flow(self.port_1.p, props.p, props.T)
|
||||
flow_2 = self.port_mass_flow(self.port_2.p, props.p, props.T)
|
||||
diagnostic_flow = flow_1 if abs(flow_1) >= abs(flow_2) else flow_2
|
||||
upstream_pressure = max(self.port_1.p, self.port_2.p, props.p, 1.0)
|
||||
density = max(self.medium.density(upstream_pressure, props.T), 1.0e-12)
|
||||
reynolds = self.reynolds_number(diagnostic_flow, props.T)
|
||||
return {
|
||||
"m": self.state.m,
|
||||
"U": self.state.U,
|
||||
"p": props.p,
|
||||
"T": props.T,
|
||||
"rho": props.rho,
|
||||
"u": props.u,
|
||||
"h": props.h,
|
||||
"re": reynolds,
|
||||
"cm": abs(diagnostic_flow) / max(self.area * upstream_pressure, 1.0e-18),
|
||||
"v": diagnostic_flow / (density * self.area),
|
||||
"ff": self.friction_factor(reynolds),
|
||||
}
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.volume)
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:port_1_pressure_flow_relation",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(
|
||||
f"{self.name}.port_1.p",
|
||||
f"{self.name}.state",
|
||||
f"{self.name}.port_1.m_flow",
|
||||
),
|
||||
role="flow",
|
||||
value=self.port_1.m_flow
|
||||
- self.port_mass_flow(self.port_1.p, props.p, props.T),
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:port_2_pressure_flow_relation",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(
|
||||
f"{self.name}.port_2.p",
|
||||
f"{self.name}.state",
|
||||
f"{self.name}.port_2.m_flow",
|
||||
),
|
||||
role="flow",
|
||||
value=self.port_2.m_flow
|
||||
- self.port_mass_flow(self.port_2.p, props.p, props.T),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class AmesimPnl0003(DynamicComponent):
|
||||
"""AMESim PNL0003 C-R-C pneumatic pipe with two end compliances."""
|
||||
|
||||
state_size = 4
|
||||
MODEL_TYPE = "amesim_pnl0003"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||||
)
|
||||
PARAMETERS = AmesimPnl0001.PARAMETERS[:-2] + (
|
||||
ParameterDefinition(
|
||||
"p1_0",
|
||||
100000.0,
|
||||
label="端口 1 初始压力",
|
||||
quantity="pressure",
|
||||
unit="Pa",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"T1_0",
|
||||
293.15,
|
||||
label="端口 1 初始温度",
|
||||
quantity="temperature",
|
||||
unit="K",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"p2_0",
|
||||
100000.0,
|
||||
label="端口 2 初始压力",
|
||||
quantity="pressure",
|
||||
unit="Pa",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"T2_0",
|
||||
293.15,
|
||||
label="端口 2 初始温度",
|
||||
quantity="temperature",
|
||||
unit="K",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition("m1", "端口 1 侧质量", "mass", "kg", "state", 10),
|
||||
ResultVariableDefinition("U1", "端口 1 侧内能", "internal_energy", "J", "state", 20),
|
||||
ResultVariableDefinition("p1", "端口 1 侧压力", "pressure", "Pa", "thermodynamic", 30),
|
||||
ResultVariableDefinition("T1", "端口 1 侧温度", "temperature", "K", "thermodynamic", 40),
|
||||
ResultVariableDefinition("rho1", "端口 1 侧密度", "density", "kg/m³", "thermodynamic", 50),
|
||||
ResultVariableDefinition("u1", "端口 1 侧比内能", "specific_internal_energy", "J/kg", "thermodynamic", 60),
|
||||
ResultVariableDefinition("h1", "端口 1 侧比焓", "specific_enthalpy", "J/kg", "thermodynamic", 70),
|
||||
ResultVariableDefinition("m2", "端口 2 侧质量", "mass", "kg", "state", 80),
|
||||
ResultVariableDefinition("U2", "端口 2 侧内能", "internal_energy", "J", "state", 90),
|
||||
ResultVariableDefinition("p2", "端口 2 侧压力", "pressure", "Pa", "thermodynamic", 100),
|
||||
ResultVariableDefinition("T2", "端口 2 侧温度", "temperature", "K", "thermodynamic", 110),
|
||||
ResultVariableDefinition("rho2", "端口 2 侧密度", "density", "kg/m³", "thermodynamic", 120),
|
||||
ResultVariableDefinition("u2", "端口 2 侧比内能", "specific_internal_energy", "J/kg", "thermodynamic", 130),
|
||||
ResultVariableDefinition("h2", "端口 2 侧比焓", "specific_enthalpy", "J/kg", "thermodynamic", 140),
|
||||
ResultVariableDefinition("dmctr", "中心质量流量", "mass_flow", "kg/s", "derived", 150),
|
||||
ResultVariableDefinition("re", "Reynolds 数", "dimensionless", "", "derived", 160),
|
||||
ResultVariableDefinition("cm", "质量流量参数", "dimensionless", "", "derived", 170),
|
||||
ResultVariableDefinition("v", "平均气体速度", "velocity", "m/s", "derived", 180),
|
||||
ResultVariableDefinition("ff", "摩擦因子", "dimensionless", "", "derived", 190),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PNL0003 C-R-C 动态管路",
|
||||
library_id="amesim",
|
||||
category_id="flow",
|
||||
symbol="pipe",
|
||||
ports=(
|
||||
PortDisplaySpec("port_1", "left", order=10),
|
||||
PortDisplaySpec("port_2", "right", order=20),
|
||||
),
|
||||
order=50,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
*,
|
||||
diam: float = 0.01,
|
||||
le: float = 1.0,
|
||||
rr: float = 1.0e-5,
|
||||
k: float = 1.35,
|
||||
kth: float = 0.0,
|
||||
extemp: float = 293.15,
|
||||
gi: float = 1.0,
|
||||
mode: float = 2.0,
|
||||
p1_0: float = 100000.0,
|
||||
T1_0: float = 293.15,
|
||||
p2_0: float = 100000.0,
|
||||
T2_0: float = 293.15,
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values(
|
||||
{
|
||||
"diam": diam,
|
||||
"le": le,
|
||||
"rr": rr,
|
||||
"k": k,
|
||||
"kth": kth,
|
||||
"extemp": extemp,
|
||||
"gi": gi,
|
||||
"mode": mode,
|
||||
"p1_0": p1_0,
|
||||
"T1_0": T1_0,
|
||||
"p2_0": p2_0,
|
||||
"T2_0": T2_0,
|
||||
}
|
||||
)
|
||||
self.medium = medium
|
||||
self.diam = float(diam)
|
||||
self.le = float(le)
|
||||
self.rr = float(rr)
|
||||
self.k = float(k)
|
||||
self.kth = float(kth)
|
||||
self.extemp = float(extemp)
|
||||
self.gi = AmesimPnl0001._integer_parameter("gi", gi)
|
||||
self.mode = AmesimPnl0001._integer_parameter("mode", mode)
|
||||
self.area = pi * self.diam * self.diam / 4.0
|
||||
self.volume = self.area * self.le
|
||||
self.compliance_volume = self.volume / 2.0
|
||||
self.exchange_area = pi * self.diam * self.le
|
||||
self.state_1 = self._initial_state(float(p1_0), float(T1_0))
|
||||
self.state_2 = self._initial_state(float(p2_0), float(T2_0))
|
||||
h1 = medium.specific_enthalpy(float(T1_0))
|
||||
h2 = medium.specific_enthalpy(float(T2_0))
|
||||
self.port_1 = self.register_declared_port("port_1")
|
||||
self.port_1.p = float(p1_0)
|
||||
self.port_1.h_outflow = h1
|
||||
self.port_2 = self.register_declared_port("port_2")
|
||||
self.port_2.p = float(p2_0)
|
||||
self.port_2.h_outflow = h2
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimPnl0003":
|
||||
return cls(name=name, medium=medium, **dict(parameters))
|
||||
|
||||
def _initial_state(self, pressure: float, temperature: float) -> VolumeState:
|
||||
mass = pressure * self.compliance_volume / (self.medium.R_gas * temperature)
|
||||
return VolumeState(m=mass, U=mass * self.medium.specific_internal_energy(temperature))
|
||||
|
||||
def get_state_vector(self) -> list[float]:
|
||||
return [*self.state_1.as_vector(), *self.state_2.as_vector()]
|
||||
|
||||
def set_state_vector(self, values: list[float]) -> None:
|
||||
if len(values) != 4:
|
||||
raise ValueError("PNL0003 state vector requires four values")
|
||||
self.state_1 = VolumeState.from_vector(values[:2])
|
||||
self.state_2 = VolumeState.from_vector(values[2:])
|
||||
|
||||
def _properties(self, state: VolumeState) -> ThermodynamicProperties:
|
||||
return self.medium.properties_from_mU(state.m, state.U, self.compliance_volume)
|
||||
|
||||
def properties_1(self) -> ThermodynamicProperties:
|
||||
props = self._properties(self.state_1)
|
||||
self.port_1.p = props.p
|
||||
self.port_1.h_outflow = props.h
|
||||
return props
|
||||
|
||||
def properties_2(self) -> ThermodynamicProperties:
|
||||
props = self._properties(self.state_2)
|
||||
self.port_2.p = props.p
|
||||
self.port_2.h_outflow = props.h
|
||||
return props
|
||||
|
||||
def refresh_thermodynamic_ports(self) -> tuple[ThermodynamicProperties, ThermodynamicProperties]:
|
||||
return self.properties_1(), self.properties_2()
|
||||
|
||||
@staticmethod
|
||||
def _dynamic_viscosity(temperature_k: float) -> float:
|
||||
return AmesimPnl00r._dynamic_viscosity(temperature_k)
|
||||
|
||||
def reynolds_number(self, mass_flow: float, temperature: float) -> float:
|
||||
viscosity = self._dynamic_viscosity(temperature)
|
||||
return 4.0 * abs(mass_flow) / (pi * self.diam * viscosity)
|
||||
|
||||
def friction_factor(self, reynolds_number: float) -> float:
|
||||
return AmesimPnl00r.friction_factor(self, reynolds_number)
|
||||
|
||||
def darcy_pressure_drop(
|
||||
self,
|
||||
mass_flow: float,
|
||||
*,
|
||||
density: float,
|
||||
temperature: float,
|
||||
) -> float:
|
||||
if mass_flow == 0.0:
|
||||
return 0.0
|
||||
reynolds = self.reynolds_number(mass_flow, temperature)
|
||||
friction = self.friction_factor(reynolds)
|
||||
velocity = mass_flow / (density * self.area)
|
||||
magnitude = friction * (self.le / self.diam) * density * velocity * velocity / 2.0
|
||||
return magnitude if mass_flow > 0.0 else -magnitude
|
||||
|
||||
def _mass_flow_for_pressure_drop(
|
||||
self,
|
||||
pressure_drop: float,
|
||||
*,
|
||||
density: float,
|
||||
temperature: float,
|
||||
) -> float:
|
||||
if pressure_drop <= 0.0:
|
||||
return 0.0
|
||||
upper = 1.0e-9
|
||||
while self.darcy_pressure_drop(upper, density=density, temperature=temperature) < pressure_drop:
|
||||
upper *= 10.0
|
||||
if upper > 1.0e3:
|
||||
raise ValueError("unable to bracket PNL0003 resistance flow")
|
||||
lower = 0.0
|
||||
for _ in range(48):
|
||||
middle = 0.5 * (lower + upper)
|
||||
if self.darcy_pressure_drop(middle, density=density, temperature=temperature) < pressure_drop:
|
||||
lower = middle
|
||||
else:
|
||||
upper = middle
|
||||
return 0.5 * (lower + upper)
|
||||
|
||||
def resistance_mass_flow(self) -> float:
|
||||
port_1 = self._properties(self.state_1)
|
||||
port_2 = self._properties(self.state_2)
|
||||
pressure_difference = port_1.p - port_2.p
|
||||
if pressure_difference == 0.0:
|
||||
return 0.0
|
||||
upstream = port_1 if pressure_difference > 0.0 else port_2
|
||||
magnitude = self._mass_flow_for_pressure_drop(
|
||||
abs(pressure_difference),
|
||||
density=upstream.rho,
|
||||
temperature=upstream.T,
|
||||
)
|
||||
return magnitude if pressure_difference > 0.0 else -magnitude
|
||||
|
||||
def _heat_flow_each(self, temperature_1: float, temperature_2: float) -> float:
|
||||
if self.mode == 1:
|
||||
return 0.0
|
||||
return self.kth * self.exchange_area * (self.extemp - 0.5 * (temperature_1 + temperature_2)) / 2.0
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
port_1 = self.properties_1()
|
||||
port_2 = self.properties_2()
|
||||
center_flow = self.resistance_mass_flow()
|
||||
upstream = port_1 if center_flow >= 0.0 else port_2
|
||||
reynolds = self.reynolds_number(center_flow, upstream.T)
|
||||
return {
|
||||
"m1": self.state_1.m,
|
||||
"U1": self.state_1.U,
|
||||
"p1": port_1.p,
|
||||
"T1": port_1.T,
|
||||
"rho1": port_1.rho,
|
||||
"u1": port_1.u,
|
||||
"h1": port_1.h,
|
||||
"m2": self.state_2.m,
|
||||
"U2": self.state_2.U,
|
||||
"p2": port_2.p,
|
||||
"T2": port_2.T,
|
||||
"rho2": port_2.rho,
|
||||
"u2": port_2.u,
|
||||
"h2": port_2.h,
|
||||
"dmctr": center_flow,
|
||||
"re": reynolds,
|
||||
"cm": abs(center_flow) / max(self.area * max(port_1.p, port_2.p, 1.0), 1.0e-18),
|
||||
"v": center_flow / (max(upstream.rho, 1.0e-12) * self.area),
|
||||
"ff": self.friction_factor(reynolds),
|
||||
}
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
port_1 = self._properties(self.state_1)
|
||||
port_2 = self._properties(self.state_2)
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:port_1_pressure_state",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="state",
|
||||
variables=(f"{self.name}.port_1.p", f"{self.name}.state"),
|
||||
role="effort",
|
||||
value=self.port_1.p - port_1.p,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:port_2_pressure_state",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="state",
|
||||
variables=(f"{self.name}.port_2.p", f"{self.name}.state"),
|
||||
role="effort",
|
||||
value=self.port_2.p - port_2.p,
|
||||
),
|
||||
)
|
||||
|
||||
def state_derivative_from_ports(self, connected_h: Mapping[str, float]) -> list[float]:
|
||||
port_1 = self.properties_1()
|
||||
port_2 = self.properties_2()
|
||||
center_flow = self.resistance_mass_flow()
|
||||
heat_flow_each = self._heat_flow_each(port_1.T, port_2.T)
|
||||
port_1_external_h = self.connection_inlet_enthalpy(
|
||||
port_m_flow=self.port_1.m_flow,
|
||||
connected_h=connected_h["port_1"],
|
||||
internal_h=port_1.h,
|
||||
)
|
||||
port_2_external_h = self.connection_inlet_enthalpy(
|
||||
port_m_flow=self.port_2.m_flow,
|
||||
connected_h=connected_h["port_2"],
|
||||
internal_h=port_2.h,
|
||||
)
|
||||
port_1_center_h = self.connection_inlet_enthalpy(
|
||||
port_m_flow=-center_flow,
|
||||
connected_h=port_2.h,
|
||||
internal_h=port_1.h,
|
||||
)
|
||||
port_2_center_h = self.connection_inlet_enthalpy(
|
||||
port_m_flow=center_flow,
|
||||
connected_h=port_1.h,
|
||||
internal_h=port_2.h,
|
||||
)
|
||||
d1 = VolumeState(
|
||||
m=self.port_1.m_flow - center_flow,
|
||||
U=self.port_1.m_flow * port_1_external_h - center_flow * port_1_center_h + heat_flow_each,
|
||||
)
|
||||
d2 = VolumeState(
|
||||
m=self.port_2.m_flow + center_flow,
|
||||
U=self.port_2.m_flow * port_2_external_h + center_flow * port_2_center_h + heat_flow_each,
|
||||
)
|
||||
return [*d1.as_vector(), *d2.as_vector()]
|
||||
@@ -18,13 +18,25 @@ LIBRARY = ComponentLibrarySpec(
|
||||
ComponentCategorySpec(id="flow", label="流动元件", order=20),
|
||||
ComponentCategorySpec(id="junctions", label="连接元件", order=30),
|
||||
ComponentCategorySpec(id="boundary", label="边界元件", order=40),
|
||||
ComponentCategorySpec(id="signals", label="信号元件", order=50),
|
||||
ComponentCategorySpec(id="mechanical", label="机械元件", order=60),
|
||||
),
|
||||
models=(
|
||||
"app.simulation.components.amesim.boundary.sources:AmesimPnpl01",
|
||||
"app.simulation.components.amesim.signals.sources:AmesimStep0",
|
||||
"app.simulation.components.amesim.signals.sources:AmesimUd00",
|
||||
"app.simulation.components.amesim.mechanical.translational:AmesimF000",
|
||||
"app.simulation.components.amesim.mechanical.translational:AmesimForc",
|
||||
"app.simulation.components.amesim.mechanical.translational:AmesimMecmas21",
|
||||
"app.simulation.components.amesim.storage.chambers:AmesimPnch023",
|
||||
"app.simulation.components.amesim.storage.chambers:AmesimPnch012",
|
||||
"app.simulation.components.amesim.flow.orifices:AmesimPnor001",
|
||||
"app.simulation.components.amesim.flow.orifices:AmesimPnvo001FixedOpening",
|
||||
"app.simulation.components.amesim.flow.orifices:AmesimPnvo001SignalOpening",
|
||||
"app.simulation.components.amesim.flow.pipes:AmesimPnl00r",
|
||||
"app.simulation.components.amesim.flow.pipes:AmesimPnl0001",
|
||||
"app.simulation.components.amesim.flow.pipes:AmesimPnl0002",
|
||||
"app.simulation.components.amesim.flow.pipes:AmesimPnl0003",
|
||||
"app.simulation.components.amesim.junctions.nodes:AmesimPn3Node2",
|
||||
"app.simulation.components.amesim.junctions.nodes:AmesimP4Node2",
|
||||
),
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""AMESim mechanical components."""
|
||||
@@ -0,0 +1,293 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from app.simulation.core.base import AlgebraicComponent, DynamicComponent
|
||||
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 IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
|
||||
|
||||
class AmesimF000(AlgebraicComponent):
|
||||
"""AMESim F000 zero force source."""
|
||||
|
||||
MODEL_TYPE = "amesim_f000"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (PortDefinition.mechanical_translational("port_1"),)
|
||||
PARAMETERS = ()
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="F000 零力源",
|
||||
library_id="amesim",
|
||||
category_id="mechanical",
|
||||
symbol="generic",
|
||||
ports=(PortDisplaySpec("port_1", "right", order=10),),
|
||||
order=10,
|
||||
)
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({})
|
||||
self.port_1 = self.register_declared_port("port_1")
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimF000":
|
||||
return cls(name=name)
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:zero_force",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.port_1.f",),
|
||||
role="flow",
|
||||
value=self.port_1.f,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class AmesimForc(AlgebraicComponent):
|
||||
"""AMESim FORC signal-to-force converter."""
|
||||
|
||||
MODEL_TYPE = "amesim_forc"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.signal("res", nominal_role="input"),
|
||||
PortDefinition.mechanical_translational("port_2"),
|
||||
)
|
||||
PARAMETERS = ()
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition("force", "输出力", "force", "N", "signal", 10),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="FORC 信号转力",
|
||||
library_id="amesim",
|
||||
category_id="mechanical",
|
||||
symbol="signal",
|
||||
ports=(
|
||||
PortDisplaySpec("res", "left", order=10),
|
||||
PortDisplaySpec("port_2", "right", order=20),
|
||||
),
|
||||
order=20,
|
||||
)
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({})
|
||||
self.res = self.register_declared_port("res")
|
||||
self.port_2 = self.register_declared_port("port_2")
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimForc":
|
||||
return cls(name=name)
|
||||
|
||||
@property
|
||||
def output_force(self) -> float:
|
||||
return float(self.res.signal)
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:signal_force",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.port_2.f", f"{self.name}.res.signal"),
|
||||
role="flow",
|
||||
value=self.port_2.f + self.output_force,
|
||||
),
|
||||
)
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
return {"force": self.output_force}
|
||||
|
||||
|
||||
class AmesimMecmas21(DynamicComponent):
|
||||
"""AMESim MECMAS21 first public one-dimensional translational mass."""
|
||||
|
||||
MODEL_TYPE = "amesim_mecmas21"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.mechanical_translational("port_1"),
|
||||
PortDefinition.mechanical_translational("port_2"),
|
||||
)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition("mass", 1.0, label="质量", quantity="mass", unit="kg", minimum=0.0, minimum_exclusive=True),
|
||||
ParameterDefinition("fstick", 0.0, label="静摩擦力", quantity="force", unit="N", minimum=0.0),
|
||||
ParameterDefinition("fcoul", 0.0, label="库仑摩擦力", quantity="force", unit="N", minimum=0.0),
|
||||
ParameterDefinition("rvisc", 0.0, label="黏性摩擦系数", quantity="translational_damping", unit="N/(m/s)", minimum=0.0),
|
||||
ParameterDefinition("wind", 0.0, label="风阻系数", quantity="windage", unit="N/(m/s)^2", minimum=0.0),
|
||||
ParameterDefinition("dvel", 1.0e-6, label="粘滞速度阈值", quantity="velocity", unit="m/s", minimum=0.0),
|
||||
ParameterDefinition("restdvel", 1.0e-6, label="恢复速度阈值", quantity="velocity", unit="m/s", minimum=0.0),
|
||||
ParameterDefinition("restcoeff", 0.65, label="恢复系数", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0),
|
||||
ParameterDefinition("astrib", 1.0e-3, label="Stribeck 常数", quantity="velocity", unit="m/s", minimum=0.0),
|
||||
ParameterDefinition("xmin", -1.0, label="下位移限位", quantity="length", unit="m"),
|
||||
ParameterDefinition("Kbmin", 1.0e9, label="下限位刚度", quantity="translational_stiffness", unit="N/m", minimum=0.0),
|
||||
ParameterDefinition("Dbmin", 1.0e4, label="下限位阻尼", quantity="translational_damping", unit="N/(m/s)", minimum=0.0),
|
||||
ParameterDefinition("Pdmin", 1.0e-4, label="下限位满阻尼穿透", quantity="length", unit="m", minimum=0.0),
|
||||
ParameterDefinition("xmax", 0.8, label="上位移限位", quantity="length", unit="m"),
|
||||
ParameterDefinition("Kbmax", 1.0e9, label="上限位刚度", quantity="translational_stiffness", unit="N/m", minimum=0.0),
|
||||
ParameterDefinition("Dbmax", 1.0e4, label="上限位阻尼", quantity="translational_damping", unit="N/(m/s)", minimum=0.0),
|
||||
ParameterDefinition("Pdmax", 1.0e-4, label="上限位满阻尼穿透", quantity="length", unit="m", minimum=0.0),
|
||||
ParameterDefinition("theta", 0.0, label="倾角", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("useFriction", 1.0, label="启用摩擦", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0),
|
||||
ParameterDefinition("stoptype", 4.0, label="限位类型", quantity="dimensionless", unit="", minimum=0.0),
|
||||
ParameterDefinition("discContactOption", 1.0, label="接触选项", quantity="dimensionless", unit="", minimum=0.0),
|
||||
ParameterDefinition("strib", 1.0, label="Stribeck 选项", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0),
|
||||
ParameterDefinition("frictionType", 1.0, label="摩擦类型", quantity="dimensionless", unit="", minimum=0.0),
|
||||
ParameterDefinition("v0", 0.0, label="初始速度", quantity="velocity", unit="m/s"),
|
||||
ParameterDefinition("x0", 0.0, label="初始位移", quantity="length", unit="m"),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition("a", "加速度", "acceleration", "m/s2", "state", 10),
|
||||
ResultVariableDefinition("v", "速度", "velocity", "m/s", "state", 20),
|
||||
ResultVariableDefinition("x", "位移", "length", "m", "state", 30),
|
||||
ResultVariableDefinition("Fvisc", "黏性摩擦力", "force", "N", "derived", 40),
|
||||
ResultVariableDefinition("Ffric", "干摩擦力", "force", "N", "derived", 50),
|
||||
ResultVariableDefinition("Fmin", "下限位力", "force", "N", "derived", 60),
|
||||
ResultVariableDefinition("Fmax", "上限位力", "force", "N", "derived", 70),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="MECMAS21 一维质量",
|
||||
library_id="amesim",
|
||||
category_id="mechanical",
|
||||
symbol="generic",
|
||||
ports=(
|
||||
PortDisplaySpec("port_1", "left", order=10),
|
||||
PortDisplaySpec("port_2", "right", order=20),
|
||||
),
|
||||
order=30,
|
||||
)
|
||||
state_size = 2
|
||||
|
||||
def __init__(self, name: str, medium: IdealGasMedium, **parameters: float) -> None:
|
||||
super().__init__(name=name)
|
||||
resolved = {definition.name: parameters.get(definition.name, definition.default) for definition in self.PARAMETERS}
|
||||
self.set_parameter_values(resolved)
|
||||
for name, value in resolved.items():
|
||||
setattr(self, name, float(value))
|
||||
self.use_friction = bool(int(self.useFriction))
|
||||
self.port_1 = self.register_declared_port("port_1")
|
||||
self.port_2 = self.register_declared_port("port_2")
|
||||
self.v = float(self.v0)
|
||||
self.x = float(self.x0)
|
||||
self.refresh_thermodynamic_ports()
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimMecmas21":
|
||||
for integer_name in ("useFriction", "stoptype", "discContactOption", "strib", "frictionType"):
|
||||
if not float(parameters[integer_name]).is_integer():
|
||||
raise ValueError(f"MECMAS21 {integer_name} must be an integer.")
|
||||
return cls(name=name, medium=medium, **dict(parameters))
|
||||
|
||||
def get_state_vector(self) -> list[float]:
|
||||
return [self.v, self.x]
|
||||
|
||||
def set_state_vector(self, values: list[float]) -> None:
|
||||
if len(values) != 2:
|
||||
raise ValueError("MECMAS21 state vector requires [v, x].")
|
||||
self.v = float(values[0])
|
||||
self.x = float(values[1])
|
||||
self.refresh_thermodynamic_ports()
|
||||
|
||||
def refresh_thermodynamic_ports(self) -> None:
|
||||
for port in (self.port_1, self.port_2):
|
||||
port.x = self.x
|
||||
port.v = self.v
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
return (
|
||||
self._state_residual("port_1", "x", self.port_1.x - self.x),
|
||||
self._state_residual("port_1", "v", self.port_1.v - self.v),
|
||||
self._state_residual("port_2", "x", self.port_2.x - self.x),
|
||||
self._state_residual("port_2", "v", self.port_2.v - self.v),
|
||||
)
|
||||
|
||||
def _state_residual(self, port_name: str, variable: str, value: float) -> EquationResidual:
|
||||
return EquationResidual(
|
||||
id=f"{self.name}:{port_name}_{variable}_state",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="state",
|
||||
variables=(f"{self.name}.{port_name}.{variable}",),
|
||||
role="effort",
|
||||
value=value,
|
||||
)
|
||||
|
||||
def _viscous_friction_force(self) -> float:
|
||||
if not self.use_friction:
|
||||
return 0.0
|
||||
return -self.rvisc * self.v
|
||||
|
||||
def _windage_force(self) -> float:
|
||||
if not self.use_friction:
|
||||
return 0.0
|
||||
return -self.wind * self.v * abs(self.v)
|
||||
|
||||
def _dry_friction_force(self) -> float:
|
||||
if not self.use_friction:
|
||||
return 0.0
|
||||
if self.v > 0.0:
|
||||
return -self.fcoul
|
||||
if self.v < 0.0:
|
||||
return self.fcoul
|
||||
return 0.0
|
||||
|
||||
def _lower_limit_force(self) -> float:
|
||||
penetration = max(self.xmin - self.x, 0.0)
|
||||
if penetration <= 0.0:
|
||||
return 0.0
|
||||
return self.Kbmin * penetration + max(-self.Dbmin * self.v, 0.0)
|
||||
|
||||
def _upper_limit_force(self) -> float:
|
||||
penetration = max(self.x - self.xmax, 0.0)
|
||||
if penetration <= 0.0:
|
||||
return 0.0
|
||||
return self.Kbmax * penetration + max(self.Dbmax * self.v, 0.0)
|
||||
|
||||
def acceleration(self) -> float:
|
||||
return (
|
||||
self.port_1.f
|
||||
+ self.port_2.f
|
||||
+ self._viscous_friction_force()
|
||||
+ self._windage_force()
|
||||
+ self._dry_friction_force()
|
||||
+ self._lower_limit_force()
|
||||
- self._upper_limit_force()
|
||||
) / self.mass
|
||||
|
||||
def state_derivative_from_ports(self, connected_h: Mapping[str, float]) -> list[float]:
|
||||
return [self.acceleration(), self.v]
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
return {
|
||||
"a": self.acceleration(),
|
||||
"v": self.v,
|
||||
"x": self.x,
|
||||
"Fvisc": self._viscous_friction_force(),
|
||||
"Ffric": self._dry_friction_force(),
|
||||
"Fmin": self._lower_limit_force(),
|
||||
"Fmax": self._upper_limit_force(),
|
||||
}
|
||||
Whitespace-only changes.
@@ -0,0 +1,204 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.metadata import ParameterDefinition, ResultVariableDefinition
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
|
||||
|
||||
class AmesimStep0(AlgebraicComponent):
|
||||
"""AMESim STEP0 scalar step signal source."""
|
||||
|
||||
MODEL_TYPE = "amesim_step0"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (PortDefinition.signal("out", nominal_role="output"),)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition("initial", 0.0, label="初始值", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("final", 1.0, label="阶跃后值", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("time", 0.0, label="阶跃时间", quantity="time", unit="s"),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition("y", "输出", "dimensionless", "", "signal", 10),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="STEP0 阶跃信号",
|
||||
library_id="amesim",
|
||||
category_id="signals",
|
||||
symbol="signal",
|
||||
ports=(PortDisplaySpec("out", "right", order=10),),
|
||||
order=10,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
*,
|
||||
initial: float = 0.0,
|
||||
final: float = 1.0,
|
||||
time: float = 0.0,
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({"initial": initial, "final": final, "time": time})
|
||||
self.initial = float(initial)
|
||||
self.final = float(final)
|
||||
self.time = float(time)
|
||||
self.out = self.register_declared_port("out")
|
||||
self.out.signal = self.output_at(0.0)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimStep0":
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
initial=parameters["initial"],
|
||||
final=parameters["final"],
|
||||
time=parameters["time"],
|
||||
)
|
||||
|
||||
def output_at(self, time: float) -> float:
|
||||
return self.final if time >= self.time else self.initial
|
||||
|
||||
def signal_output_values(self, time: float) -> dict[str, float]:
|
||||
return {"out": self.output_at(time)}
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
return {"y": self.out.signal}
|
||||
|
||||
|
||||
class AmesimUd00(AlgebraicComponent):
|
||||
"""AMESim UD00 piecewise-linear scalar signal source."""
|
||||
|
||||
MODEL_TYPE = "amesim_ud00"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (PortDefinition.signal("out", nominal_role="output"),)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition("tstart", 0.0, label="启动时间", quantity="time", unit="s"),
|
||||
ParameterDefinition("start1", 0.0, label="第 1 段起点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("end1", 1.0, label="第 1 段终点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("t1", 1.0, label="第 1 段时长", quantity="time", unit="s", minimum=0.0),
|
||||
ParameterDefinition("start2", 1.0, label="第 2 段起点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("end2", 1.0, label="第 2 段终点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("t2", 0.0, label="第 2 段时长", quantity="time", unit="s", minimum=0.0),
|
||||
ParameterDefinition("start3", 1.0, label="第 3 段起点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("end3", 1.0, label="第 3 段终点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("t3", 0.0, label="第 3 段时长", quantity="time", unit="s", minimum=0.0),
|
||||
ParameterDefinition("start4", 1.0, label="第 4 段起点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("end4", 1.0, label="第 4 段终点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("t4", 0.0, label="第 4 段时长", quantity="time", unit="s", minimum=0.0),
|
||||
ParameterDefinition("start5", 1.0, label="第 5 段起点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("end5", 1.0, label="第 5 段终点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("t5", 0.0, label="第 5 段时长", quantity="time", unit="s", minimum=0.0),
|
||||
ParameterDefinition("start6", 1.0, label="第 6 段起点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("end6", 1.0, label="第 6 段终点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("t6", 0.0, label="第 6 段时长", quantity="time", unit="s", minimum=0.0),
|
||||
ParameterDefinition("start7", 1.0, label="第 7 段起点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("end7", 1.0, label="第 7 段终点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("t7", 0.0, label="第 7 段时长", quantity="time", unit="s", minimum=0.0),
|
||||
ParameterDefinition("start8", 1.0, label="第 8 段起点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("end8", 1.0, label="第 8 段终点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("t8", 0.0, label="第 8 段时长", quantity="time", unit="s", minimum=0.0),
|
||||
ParameterDefinition("nstages", 1.0, label="段数", quantity="dimensionless", unit="", minimum=1.0, maximum=8.0),
|
||||
ParameterDefinition("iscyclic", 0.0, label="循环", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition("y", "输出", "dimensionless", "", "signal", 10),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="UD00 分段线性信号",
|
||||
library_id="amesim",
|
||||
category_id="signals",
|
||||
symbol="signal",
|
||||
ports=(PortDisplaySpec("out", "right", order=10),),
|
||||
order=20,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
*,
|
||||
tstart: float = 0.0,
|
||||
starts: tuple[float, ...] = (0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0),
|
||||
ends: tuple[float, ...] = (1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0),
|
||||
durations: tuple[float, ...] = (1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||
nstages: int = 1,
|
||||
iscyclic: bool = False,
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
if len(starts) != 8 or len(ends) != 8 or len(durations) != 8:
|
||||
raise ValueError("UD00 requires exactly eight start, end, and duration values.")
|
||||
if nstages < 1 or nstages > 8:
|
||||
raise ValueError("UD00 nstages must be between 1 and 8.")
|
||||
self.tstart = float(tstart)
|
||||
self.starts = tuple(float(value) for value in starts)
|
||||
self.ends = tuple(float(value) for value in ends)
|
||||
self.durations = tuple(float(value) for value in durations)
|
||||
self.nstages = int(nstages)
|
||||
self.iscyclic = bool(iscyclic)
|
||||
values: dict[str, float] = {"tstart": self.tstart, "nstages": float(self.nstages), "iscyclic": float(int(self.iscyclic))}
|
||||
for index in range(1, 9):
|
||||
values[f"start{index}"] = self.starts[index - 1]
|
||||
values[f"end{index}"] = self.ends[index - 1]
|
||||
values[f"t{index}"] = self.durations[index - 1]
|
||||
self.set_parameter_values(values)
|
||||
self.out = self.register_declared_port("out")
|
||||
self.out.signal = self.output_at(0.0)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimUd00":
|
||||
nstages = parameters["nstages"]
|
||||
iscyclic = parameters["iscyclic"]
|
||||
if not float(nstages).is_integer():
|
||||
raise ValueError("UD00 nstages must be an integer.")
|
||||
if not float(iscyclic).is_integer():
|
||||
raise ValueError("UD00 iscyclic must be 0 or 1.")
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
tstart=parameters["tstart"],
|
||||
starts=tuple(parameters[f"start{index}"] for index in range(1, 9)),
|
||||
ends=tuple(parameters[f"end{index}"] for index in range(1, 9)),
|
||||
durations=tuple(parameters[f"t{index}"] for index in range(1, 9)),
|
||||
nstages=int(nstages),
|
||||
iscyclic=bool(int(iscyclic)),
|
||||
)
|
||||
|
||||
def output_at(self, time: float) -> float:
|
||||
elapsed = max(float(time) - self.tstart, 0.0)
|
||||
active_durations = self.durations[: self.nstages]
|
||||
total_duration = sum(active_durations)
|
||||
if self.iscyclic and total_duration > 0.0:
|
||||
elapsed = elapsed % total_duration
|
||||
|
||||
stage_start_time = 0.0
|
||||
for index, duration in enumerate(active_durations):
|
||||
stage_end_time = stage_start_time + duration
|
||||
if elapsed < stage_end_time or index == self.nstages - 1:
|
||||
if duration <= 0.0:
|
||||
return self.ends[index]
|
||||
fraction = (elapsed - stage_start_time) / duration
|
||||
return self.starts[index] + fraction * (self.ends[index] - self.starts[index])
|
||||
stage_start_time = stage_end_time
|
||||
return self.ends[self.nstages - 1]
|
||||
|
||||
def signal_output_values(self, time: float) -> dict[str, float]:
|
||||
return {"out": self.output_at(time)}
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
return {"y": self.out.signal}
|
||||
@@ -8,6 +8,7 @@ from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.metadata import (
|
||||
ParameterDefinition,
|
||||
ResultVariableDefinition,
|
||||
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
|
||||
)
|
||||
from app.simulation.core.medium import IdealGasMedium, ThermodynamicProperties
|
||||
@@ -248,3 +249,273 @@ class AmesimPnch023(ThermodynamicVolumeComponent):
|
||||
value=self.port_2.p - pressure,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
MODEL_TYPE = "amesim_pnch012"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_3", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_4", nominal_role="bidirectional"),
|
||||
)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition(
|
||||
"cvol0",
|
||||
0.015,
|
||||
label="死容积",
|
||||
quantity="volume",
|
||||
unit="m3",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"kth",
|
||||
0.0,
|
||||
label="换热系数",
|
||||
quantity="heat_transfer_coefficient",
|
||||
unit="W/(m2*K)",
|
||||
minimum=0.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"sth",
|
||||
0.1,
|
||||
label="换热面积",
|
||||
quantity="area",
|
||||
unit="m2",
|
||||
minimum=0.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"extemp",
|
||||
293.15,
|
||||
label="外部温度",
|
||||
quantity="temperature",
|
||||
unit="K",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"gi",
|
||||
1.0,
|
||||
label="气体类型索引",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=1.0,
|
||||
maximum=99.0,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"p0",
|
||||
100000.0,
|
||||
label="初始压力",
|
||||
quantity="pressure",
|
||||
unit="Pa",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition(
|
||||
"T0",
|
||||
293.15,
|
||||
label="初始温度",
|
||||
quantity="temperature",
|
||||
unit="K",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
),
|
||||
ParameterDefinition("vol1", 0.0, label="端口 1 外部容积", quantity="volume", unit="m3"),
|
||||
ParameterDefinition("vol2", 0.0, label="端口 2 外部容积", quantity="volume", unit="m3"),
|
||||
ParameterDefinition("vol3", 0.0, label="端口 3 外部容积", quantity="volume", unit="m3"),
|
||||
ParameterDefinition("vol4", 0.0, label="端口 4 外部容积", quantity="volume", unit="m3"),
|
||||
ParameterDefinition("dvol1", 0.0, label="端口 1 容积变化率", quantity="volume_flow", unit="m3/s"),
|
||||
ParameterDefinition("dvol2", 0.0, label="端口 2 容积变化率", quantity="volume_flow", unit="m3/s"),
|
||||
ParameterDefinition("dvol3", 0.0, label="端口 3 容积变化率", quantity="volume_flow", unit="m3/s"),
|
||||
ParameterDefinition("dvol4", 0.0, label="端口 4 容积变化率", quantity="volume_flow", unit="m3/s"),
|
||||
)
|
||||
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES + (
|
||||
ResultVariableDefinition("vol", "气室总容积", "volume", "m3", "derived", 100),
|
||||
ResultVariableDefinition("dvol", "总容积变化率", "volume_flow", "m3/s", "derived", 110),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PNCH012 变容气室",
|
||||
library_id="amesim",
|
||||
category_id="storage",
|
||||
symbol="tank",
|
||||
ports=(
|
||||
PortDisplaySpec("port_1", "left", order=10),
|
||||
PortDisplaySpec("port_2", "right", order=20),
|
||||
PortDisplaySpec("port_3", "left", order=30),
|
||||
PortDisplaySpec("port_4", "right", order=40),
|
||||
),
|
||||
order=20,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
*,
|
||||
cvol0: float = 0.015,
|
||||
kth: float = 0.0,
|
||||
sth: float = 0.1,
|
||||
extemp: float = 293.15,
|
||||
gi: float = 1.0,
|
||||
p0: float = 100000.0,
|
||||
T0: float = 293.15,
|
||||
vol1: float = 0.0,
|
||||
vol2: float = 0.0,
|
||||
vol3: float = 0.0,
|
||||
vol4: float = 0.0,
|
||||
dvol1: float = 0.0,
|
||||
dvol2: float = 0.0,
|
||||
dvol3: float = 0.0,
|
||||
dvol4: float = 0.0,
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values(
|
||||
{
|
||||
"cvol0": cvol0,
|
||||
"kth": kth,
|
||||
"sth": sth,
|
||||
"extemp": extemp,
|
||||
"gi": gi,
|
||||
"p0": p0,
|
||||
"T0": T0,
|
||||
"vol1": vol1,
|
||||
"vol2": vol2,
|
||||
"vol3": vol3,
|
||||
"vol4": vol4,
|
||||
"dvol1": dvol1,
|
||||
"dvol2": dvol2,
|
||||
"dvol3": dvol3,
|
||||
"dvol4": dvol4,
|
||||
}
|
||||
)
|
||||
self.medium = medium
|
||||
self.cvol0 = float(cvol0)
|
||||
self.kth = float(kth)
|
||||
self.sth = float(sth)
|
||||
self.extemp = float(extemp)
|
||||
self.gi = AmesimPnch023._integer_parameter("gi", gi)
|
||||
self.p0 = float(p0)
|
||||
self.T0 = float(T0)
|
||||
self.external_volumes = {
|
||||
"port_1": float(vol1),
|
||||
"port_2": float(vol2),
|
||||
"port_3": float(vol3),
|
||||
"port_4": float(vol4),
|
||||
}
|
||||
self.external_volume_rates = {
|
||||
"port_1": float(dvol1),
|
||||
"port_2": float(dvol2),
|
||||
"port_3": float(dvol3),
|
||||
"port_4": float(dvol4),
|
||||
}
|
||||
if self.total_volume() <= 0.0:
|
||||
raise ValueError("PNCH012 total volume must be positive.")
|
||||
m0 = self.p0 * self.total_volume() / (medium.R_gas * self.T0)
|
||||
U0 = m0 * medium.specific_internal_energy(self.T0)
|
||||
self.state = VolumeState(m=m0, U=U0)
|
||||
initial_h = medium.specific_enthalpy(self.T0)
|
||||
for port_name in ("port_1", "port_2", "port_3", "port_4"):
|
||||
port = self.register_declared_port(port_name)
|
||||
port.p = self.p0
|
||||
port.h_outflow = initial_h
|
||||
setattr(self, port_name, port)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimPnch012":
|
||||
return cls(name=name, medium=medium, **dict(parameters))
|
||||
|
||||
def total_volume(self) -> float:
|
||||
minimum_volume = self.cvol0 / 100.0
|
||||
return max(self.cvol0 + sum(self.external_volumes.values()), 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())
|
||||
|
||||
def get_state_vector(self) -> list[float]:
|
||||
return self.state.as_vector()
|
||||
|
||||
def set_state_vector(self, values: list[float]) -> None:
|
||||
self.state = VolumeState.from_vector(values)
|
||||
|
||||
def properties(self) -> ThermodynamicProperties:
|
||||
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.total_volume())
|
||||
for port_name in ("port_1", "port_2", "port_3", "port_4"):
|
||||
port = self.get_port(port_name)
|
||||
port.p = props.p
|
||||
port.h_outflow = props.h
|
||||
return props
|
||||
|
||||
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
|
||||
return self.properties()
|
||||
|
||||
def thermal_energy_flow_w(self, temperature: float) -> float:
|
||||
return self.kth * self.sth * (self.extemp - temperature)
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
props = self.properties()
|
||||
return {
|
||||
"m": self.state.m,
|
||||
"U": self.state.U,
|
||||
"p": props.p,
|
||||
"T": props.T,
|
||||
"rho": props.rho,
|
||||
"u": props.u,
|
||||
"h": props.h,
|
||||
"vol": self.total_volume(),
|
||||
"dvol": self.total_volume_rate(),
|
||||
}
|
||||
|
||||
def state_derivative_from_ports(self, connected_h: Mapping[str, float]) -> list[float]:
|
||||
props = self.properties()
|
||||
mass_derivative = 0.0
|
||||
energy_derivative = 0.0
|
||||
for port_name in ("port_1", "port_2", "port_3", "port_4"):
|
||||
port = self.get_port(port_name)
|
||||
inlet_h = self.connection_inlet_enthalpy(
|
||||
port_m_flow=port.m_flow,
|
||||
connected_h=connected_h[port_name],
|
||||
internal_h=props.h,
|
||||
)
|
||||
mass_derivative += port.m_flow
|
||||
energy_derivative += port.m_flow * inlet_h
|
||||
energy_derivative += self.thermal_energy_flow_w(props.T)
|
||||
energy_derivative -= props.p * self.total_volume_rate()
|
||||
return VolumeState(m=mass_derivative, U=energy_derivative).as_vector()
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
pressure = self.medium.properties_from_mU(
|
||||
self.state.m,
|
||||
self.state.U,
|
||||
self.total_volume(),
|
||||
).p
|
||||
return tuple(
|
||||
EquationResidual(
|
||||
id=f"{self.name}:{port_name}_pressure_state",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="state",
|
||||
variables=(f"{self.name}.{port_name}.p", f"{self.name}.state"),
|
||||
role="effort",
|
||||
value=self.get_port(port_name).p - pressure,
|
||||
)
|
||||
for port_name in ("port_1", "port_2", "port_3", "port_4")
|
||||
)
|
||||
@@ -9,10 +9,12 @@ ResultVariableScope = Literal["component", "port"]
|
||||
|
||||
|
||||
SI_UNIT_BY_QUANTITY: dict[str, str] = {
|
||||
"acceleration": "m/s2",
|
||||
"area": "m2",
|
||||
"dimensionless": "",
|
||||
"density": "kg/m³",
|
||||
"flow_coefficient": "kg/(s*Pa^0.5)",
|
||||
"force": "N",
|
||||
"heat_transfer_coefficient": "W/(m2*K)",
|
||||
"internal_energy": "J",
|
||||
"length": "m",
|
||||
@@ -22,8 +24,13 @@ SI_UNIT_BY_QUANTITY: dict[str, str] = {
|
||||
"specific_enthalpy": "J/kg",
|
||||
"specific_internal_energy": "J/kg",
|
||||
"temperature": "K",
|
||||
"translational_damping": "N/(m/s)",
|
||||
"translational_stiffness": "N/m",
|
||||
"time": "s",
|
||||
"velocity": "m/s",
|
||||
"volume": "m3",
|
||||
"volume_flow": "m3/s",
|
||||
"windage": "N/(m/s)^2",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -90,6 +90,76 @@ class PortDefinition:
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def mechanical_translational(
|
||||
cls,
|
||||
name: str,
|
||||
*,
|
||||
nominal_role: Literal["inlet", "outlet", "bidirectional"] = "bidirectional",
|
||||
) -> PortDefinition:
|
||||
return cls(
|
||||
name=name,
|
||||
kind="physical",
|
||||
domain="mechanical",
|
||||
nominal_role=nominal_role,
|
||||
positive_flow_direction="intoComponent",
|
||||
variables=(
|
||||
PortVariableDefinition(
|
||||
"x",
|
||||
"effort",
|
||||
"equal",
|
||||
label="位移",
|
||||
quantity="length",
|
||||
unit="m",
|
||||
order=10,
|
||||
),
|
||||
PortVariableDefinition(
|
||||
"v",
|
||||
"effort",
|
||||
"equal",
|
||||
label="速度",
|
||||
quantity="velocity",
|
||||
unit="m/s",
|
||||
order=20,
|
||||
),
|
||||
PortVariableDefinition(
|
||||
"f",
|
||||
"flow",
|
||||
"sumToZero",
|
||||
label="力",
|
||||
quantity="force",
|
||||
unit="N",
|
||||
order=30,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def signal(
|
||||
cls,
|
||||
name: str,
|
||||
*,
|
||||
nominal_role: Literal["input", "output"],
|
||||
domain: str = "signal",
|
||||
) -> PortDefinition:
|
||||
return cls(
|
||||
name=name,
|
||||
kind="signal",
|
||||
domain=domain,
|
||||
nominal_role=nominal_role,
|
||||
variables=(
|
||||
PortVariableDefinition(
|
||||
"signal",
|
||||
"signal",
|
||||
"directed",
|
||||
label="信号值",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
order=10,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
def as_interface_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"name": self.name,
|
||||
@@ -108,6 +178,10 @@ class PortState:
|
||||
p: float = 0.0
|
||||
m_flow: float = 0.0
|
||||
h_outflow: float = 0.0
|
||||
signal: float = 0.0
|
||||
x: float = 0.0
|
||||
v: float = 0.0
|
||||
f: float = 0.0
|
||||
definition: PortDefinition | None = field(default=None, repr=False, compare=False)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -134,12 +134,12 @@ class PressureFlowSolver:
|
||||
if not changed:
|
||||
break
|
||||
|
||||
def _scales(self) -> tuple[float, float]:
|
||||
def _scales(self) -> dict[str, float]:
|
||||
pressure_scale = max(
|
||||
[
|
||||
abs(unknown.read())
|
||||
for unknown in self.unknowns
|
||||
if unknown.role == "effort" and unknown.read() > 0.0
|
||||
if unknown.variable == "p" and unknown.read() > 0.0
|
||||
]
|
||||
+ [1e5]
|
||||
)
|
||||
@@ -148,16 +148,31 @@ class PressureFlowSolver:
|
||||
for component in self.network.components.values()
|
||||
if hasattr(component, "K_eff")
|
||||
]
|
||||
flow_scale = max(
|
||||
mass_flow_scale = max(
|
||||
estimated_flows
|
||||
+ [
|
||||
abs(unknown.read())
|
||||
for unknown in self.unknowns
|
||||
if unknown.role == "flow"
|
||||
if unknown.variable == "m_flow"
|
||||
]
|
||||
+ [1e-3]
|
||||
)
|
||||
return pressure_scale, flow_scale
|
||||
return {
|
||||
"p": pressure_scale,
|
||||
"m_flow": mass_flow_scale,
|
||||
"x": max(
|
||||
[abs(unknown.read()) for unknown in self.unknowns if unknown.variable == "x"]
|
||||
+ [1.0]
|
||||
),
|
||||
"v": max(
|
||||
[abs(unknown.read()) for unknown in self.unknowns if unknown.variable == "v"]
|
||||
+ [1.0]
|
||||
),
|
||||
"f": max(
|
||||
[abs(unknown.read()) for unknown in self.unknowns if unknown.variable == "f"]
|
||||
+ [1.0]
|
||||
),
|
||||
}
|
||||
|
||||
def solve(self) -> AlgebraicSolveDiagnostics:
|
||||
try:
|
||||
@@ -169,11 +184,13 @@ class PressureFlowSolver:
|
||||
) from exc
|
||||
|
||||
self._seed_equal_pressures()
|
||||
pressure_scale, flow_scale = self._scales()
|
||||
scales = self._scales()
|
||||
pressure_scale = scales["p"]
|
||||
flow_scale = scales["m_flow"]
|
||||
positive_pressures = [
|
||||
unknown.read()
|
||||
for unknown in self.unknowns
|
||||
if unknown.role == "effort" and unknown.read() > 0.0
|
||||
if unknown.variable == "p" and unknown.read() > 0.0
|
||||
]
|
||||
fallback_pressure = (
|
||||
sum(positive_pressures) / len(positive_pressures)
|
||||
@@ -182,13 +199,28 @@ class PressureFlowSolver:
|
||||
)
|
||||
|
||||
def variable_scale(unknown: AlgebraicUnknown) -> float:
|
||||
return pressure_scale if unknown.role == "effort" else flow_scale
|
||||
return scales.get(unknown.variable, max(abs(unknown.read()), 1.0))
|
||||
|
||||
def equation_scale(equation) -> float:
|
||||
variable_names = [
|
||||
variable.rsplit(".", 1)[-1]
|
||||
for variable in equation.variables
|
||||
]
|
||||
if equation.role == "flow":
|
||||
return scales["f"] if "f" in variable_names else flow_scale
|
||||
if equation.role == "effort":
|
||||
if "x" in variable_names:
|
||||
return scales["x"]
|
||||
if "v" in variable_names:
|
||||
return scales["v"]
|
||||
return pressure_scale
|
||||
return max([scales.get(name, 1.0) for name in variable_names] + [1.0])
|
||||
|
||||
x0 = np.asarray(
|
||||
[
|
||||
(
|
||||
unknown.read()
|
||||
if unknown.role != "effort" or unknown.read() > 0.0
|
||||
if unknown.variable != "p" or unknown.read() > 0.0
|
||||
else fallback_pressure
|
||||
)
|
||||
/ variable_scale(unknown)
|
||||
@@ -198,7 +230,7 @@ class PressureFlowSolver:
|
||||
)
|
||||
lower = np.asarray(
|
||||
[
|
||||
1.0 / pressure_scale if unknown.role == "effort" else -np.inf
|
||||
1.0 / pressure_scale if unknown.variable == "p" else -np.inf
|
||||
for unknown in self.unknowns
|
||||
]
|
||||
)
|
||||
@@ -213,8 +245,7 @@ class PressureFlowSolver:
|
||||
equations = self.network.pressure_flow_equation_residuals()
|
||||
return np.asarray(
|
||||
[
|
||||
equation.value
|
||||
/ (pressure_scale if equation.role == "effort" else flow_scale)
|
||||
equation.value / equation_scale(equation)
|
||||
for equation in equations
|
||||
],
|
||||
dtype=float,
|
||||
@@ -234,8 +265,7 @@ class PressureFlowSolver:
|
||||
equations = self.network.pressure_flow_equation_residuals()
|
||||
scaled = [
|
||||
abs(
|
||||
equation.value
|
||||
/ (pressure_scale if equation.role == "effort" else flow_scale)
|
||||
equation.value / equation_scale(equation)
|
||||
)
|
||||
for equation in equations
|
||||
]
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol
|
||||
|
||||
from app.simulation.systems.network import Endpoint, SimulationNetwork
|
||||
|
||||
|
||||
class SignalOutputComponent(Protocol):
|
||||
name: str
|
||||
|
||||
def signal_output_values(self, time: float) -> dict[str, float]:
|
||||
...
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SignalSolveDiagnostics:
|
||||
propagated: int
|
||||
|
||||
def as_dict(self) -> dict[str, object]:
|
||||
return {"propagated": self.propagated}
|
||||
|
||||
|
||||
class SignalResolver:
|
||||
"""Propagate scalar signal connections from output ports to input ports."""
|
||||
|
||||
def __init__(self, network: SimulationNetwork) -> None:
|
||||
self.network = network
|
||||
self._connections = [
|
||||
connection for connection in network.connections if connection.kind == "signal"
|
||||
]
|
||||
self.last_diagnostics: SignalSolveDiagnostics | None = None
|
||||
|
||||
def solve(self, time: float) -> SignalSolveDiagnostics:
|
||||
for component in self.network.components.values():
|
||||
signal_output_values = getattr(component, "signal_output_values", None)
|
||||
if signal_output_values is None:
|
||||
continue
|
||||
for port_name, value in signal_output_values(time).items():
|
||||
component.get_port(port_name).signal = float(value)
|
||||
|
||||
propagated = 0
|
||||
for connection in self._connections:
|
||||
source, target = self._source_target(connection.endpoints)
|
||||
source_port = self.network.components[source.component].get_port(source.port)
|
||||
target_port = self.network.components[target.component].get_port(target.port)
|
||||
target_port.signal = source_port.signal
|
||||
propagated += 1
|
||||
|
||||
diagnostics = SignalSolveDiagnostics(propagated=propagated)
|
||||
self.last_diagnostics = diagnostics
|
||||
return diagnostics
|
||||
|
||||
def _source_target(self, endpoints: tuple[Endpoint, Endpoint]) -> tuple[Endpoint, Endpoint]:
|
||||
first, second = endpoints
|
||||
first_port = self.network.components[first.component].get_port(first.port)
|
||||
second_port = self.network.components[second.component].get_port(second.port)
|
||||
if first_port.definition is not None and first_port.definition.nominal_role == "output":
|
||||
return first, second
|
||||
if second_port.definition is not None and second_port.definition.nominal_role == "output":
|
||||
return second, first
|
||||
raise ValueError("Signal connection must contain one output endpoint.")
|
||||
@@ -9,6 +9,7 @@ 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.solver import ODESolution, SolveIVPConfig, integrate_ode
|
||||
from app.simulation.solvers.signal import SignalResolver
|
||||
from app.simulation.solvers.stream import StreamResolver
|
||||
from app.simulation.systems.network import Endpoint, SimulationNetwork
|
||||
|
||||
@@ -111,18 +112,6 @@ def simulation_preparation_issues(
|
||||
)
|
||||
)
|
||||
|
||||
if any(
|
||||
definition.kind == "signal"
|
||||
for component in network.components.values()
|
||||
for definition in component.port_definitions
|
||||
):
|
||||
issues.append(
|
||||
SimulationPreparationIssue(
|
||||
"SIGNAL_PORT_UNSUPPORTED",
|
||||
"Signal-port simulation is not implemented in the current MVP solver.",
|
||||
)
|
||||
)
|
||||
|
||||
structure = network.pressure_flow_structure_dict()
|
||||
if not structure["isSquare"]:
|
||||
issues.append(
|
||||
@@ -244,11 +233,13 @@ class GenericFluidSystem:
|
||||
self.network = network
|
||||
self.dynamic_components = network.dynamic_components()
|
||||
self.pressure_flow_solver = PressureFlowSolver(network)
|
||||
self.signal_resolver = SignalResolver(network)
|
||||
self.stream_resolver = StreamResolver(network)
|
||||
self.algebraic_solve_count = 0
|
||||
self.max_algebraic_residual = 0.0
|
||||
self.max_algebraic_evaluations = 0
|
||||
self.max_stream_iterations = 0
|
||||
self.signal_propagation_count = 0
|
||||
|
||||
def initial_state_vector(self) -> list[float]:
|
||||
return self.network.initial_state_vector()
|
||||
@@ -256,7 +247,9 @@ class GenericFluidSystem:
|
||||
def apply_state_vector(self, values: list[float]) -> None:
|
||||
self.network.apply_state_vector(values)
|
||||
|
||||
def _close_current_state(self) -> dict[str, dict[str, float]]:
|
||||
def _close_current_state(self, time: float) -> dict[str, dict[str, float]]:
|
||||
signal = self.signal_resolver.solve(time)
|
||||
self.signal_propagation_count += signal.propagated
|
||||
for component in self.dynamic_components:
|
||||
component.refresh_thermodynamic_ports()
|
||||
algebraic = self.pressure_flow_solver.solve()
|
||||
@@ -279,12 +272,12 @@ class GenericFluidSystem:
|
||||
def consistent_initial_state_vector(self) -> list[float]:
|
||||
state = self.initial_state_vector()
|
||||
self.apply_state_vector(state)
|
||||
self._close_current_state()
|
||||
self._close_current_state(0.0)
|
||||
return state
|
||||
|
||||
def rhs(self, _time: float, state_vector: list[float]) -> list[float]:
|
||||
self.apply_state_vector(state_vector)
|
||||
connected_h = self._close_current_state()
|
||||
connected_h = self._close_current_state(_time)
|
||||
derivatives: list[float] = []
|
||||
for component in self.dynamic_components:
|
||||
derivatives.extend(
|
||||
@@ -395,7 +388,7 @@ class GenericFluidSystem:
|
||||
]
|
||||
try:
|
||||
self.apply_state_vector(state)
|
||||
self._close_current_state()
|
||||
self._close_current_state(times[time_index])
|
||||
self._append_current_state(series)
|
||||
series["time"].append(times[time_index])
|
||||
except Exception as exc:
|
||||
@@ -433,6 +426,14 @@ class GenericFluidSystem:
|
||||
else None
|
||||
),
|
||||
},
|
||||
"signal": {
|
||||
"propagations": self.signal_propagation_count,
|
||||
"last": (
|
||||
self.signal_resolver.last_diagnostics.as_dict()
|
||||
if self.signal_resolver.last_diagnostics is not None
|
||||
else None
|
||||
),
|
||||
},
|
||||
"stateCount": len(initial_state),
|
||||
"sampleCount": len(series["time"]),
|
||||
}
|
||||
|
||||
@@ -31,25 +31,25 @@
|
||||
| AMESim 子模型 | 数量 | AMESim 角色 | 当前公开状态 | 建议目标 | 先决条件 / 限制 |
|
||||
| --- | ---: | --- | --- | --- | --- |
|
||||
| `PNCH023` | 4 | 固定容积气室,带换热 | 候选公开 | `amesim_pnch023`,`storage` | 可基于 `ThermodynamicVolumeComponent`;需按 AMESim `cvol/extemp/kth/sth/gi` 复核质量、能量、换热方程。 |
|
||||
| `PNCH012` | 8 | 变容气室,带换热 | 暂不公开 | 内部 `test_mql` 模型;后续 `amesim_pnch012` | 体积由机械/活塞运动耦合;公开前需要机械域或明确外部体积输入协议。 |
|
||||
| `PNCH012` | 8 | 变容气室,带换热 | 参数化第一版公开 | `amesim_pnch012`,`storage` | 已将 AMESim `vol1..4/dvol1..4` 外部体积输入映射为 SI 参数,支持固定/预设体积场景;实时机械耦合仍需后续机械域或输入端口协议。 |
|
||||
| `PNOR001` | 8 | 常系数气动孔口 | 候选公开 | `amesim_pnor001`,`flow` | 可基于 `AlgebraicComponent`;需按 AMESim `cq/area/Cv/Kv/flowset/gi` 复核双向流、零压差正则化和单位换算。 |
|
||||
| `PNVO001` | 8 | 信号调制气动孔口 | 暂不公开 | 内部固定算例;后续 `amesim_pnvo001` | 依赖信号端口和事件/阶跃控制;当前公开组件库不支持信号仿真。可先拆出无信号开度参数版本,但不得等同 AMESim `PNVO001`。 |
|
||||
| `PNVO001` | 8 | 信号调制气动孔口 | 第一版公开 | `amesim_pnvo001`,`flow` | 已接入标量信号端口 `res`,可由 `amesim_step0` 驱动开度;精确事件语义和 AMESim baseline 仍留后续修模。固定开度变体 `amesim_pnvo001_fixed` 继续保留。 |
|
||||
| `PN3NODE2` | 8 | 三端气动节点,压力温度由 port 2 固定 | 候选公开 | `amesim_pn3node2`,`junctions` | 当前 `Tee` 是通用三通近似;AMESim port 2 参考温压语义和 stream 混合需单独测试。 |
|
||||
| `P4NODE2` | 8 | 四端气动节点,压力温度由 port 2 固定 | 候选公开 | `amesim_p4node2`,`junctions` | 需要新增四端 junction 基类/模型;复核 port 2 参考温压和多支路混合。 |
|
||||
| `PNL00R` | 4 | 管路纯阻性摩擦段 | 候选公开 | `amesim_pnl00r`,`flow` | 可基于准稳态阻性管;需按 AMESim `PNL00R` 参数和摩擦公式复核。 |
|
||||
| `PNL0001` | 20 | C-R 动态管路 | 先内部 | 后续 `amesim_pnl0001` | 含管内储气动态和摩擦耦合;当前 `pipe` 只是准稳态阻性模型,不能直接宣称等价。 |
|
||||
| `PNL0002` | 8 | R-C-R 动态管路 | 先内部 | 后续 `amesim_pnl0002` | 需要多段动态状态和端口/中心阻力结构测试。 |
|
||||
| `PNL0003` | 8 | C-R-C 动态管路 | 先内部 | 后续 `amesim_pnl0003` | 当前 `test_mql` 仍在诊断事件早期导数差异;公开前必须完成 AMESim baseline 对齐。 |
|
||||
| `PNPL01` | 16 | 零气动流源 | 暂不公开 | 后续边界/源元件 | 当前网络要求物理端口连接并由组件/储能闭合;需设计一端口源/边界组件契约。 |
|
||||
| `PNL0001` | 20 | C-R 动态管路 | 第一版公开 | `amesim_pnl0001`,`flow` | 已按公开契约接入两状态管内容积 + port 1 摩擦残差 + mode 2 换热项;仍需后续按 AMESim baseline 复核 `pn2pipefr_` 和 mode 1 多方语义。 |
|
||||
| `PNL0002` | 8 | R-C-R 动态管路 | 第一版公开 | `amesim_pnl0002`,`flow` | 已按公开契约接入中心两状态容积 + 两端半长摩擦残差 + mode 2 换热项;仍需后续按 AMESim baseline 复核 `pn2pipefr_` 和 mode 1 多方语义。 |
|
||||
| `PNL0003` | 8 | C-R-C 动态管路 | 第一版公开 | `amesim_pnl0003`,`flow` | 已按公开契约接入两端四状态容积 + 中心摩擦流 + mode 2 换热项;大压差动态闭合和 AMESim baseline 误差仍留后续修模。 |
|
||||
| `PNPL01` | 16 | 零气动流源 | 第一版公开 | `amesim_pnpl01`,`boundary` | 当前实现一端零流边界,只约束端口质量流量为 0;压力源/外部边界语义留后续扩展。 |
|
||||
| `PNGD00` | 1 | 氦气气体定义 | 暂不公开 | medium 配置,不是画布物理组件 | 应映射为系统/介质设置;不能作为普通可连接组件注册。 |
|
||||
| `PNRP17` | 8 | 气动活塞与移动体耦合 | 暂不公开 | 内部固定算例;后续跨域组件 | 依赖气动端口和机械端口耦合;需要新增机械域、跨域状态和连接规则。 |
|
||||
| `MECMAS21` | 10 | 一维平动质量 | 暂不公开 | 后续机械域组件 | 当前无机械端口、力/速度/位移连接规则和机械求解器。 |
|
||||
| `MECMAS21` | 10 | 一维平动质量 | 第一版公开 | `amesim_mecmas21`,`mechanical` | 已接入一维机械端口 `x/v/f`、双端质量状态和基本摩擦/限位项,并跑通零力源与信号力源最小 System XML;完整 AMESim 接触/事件语义仍留后续对齐。 |
|
||||
| `LMECHN1` | 2 | 动态线性机械节点 | 暂不公开 | 后续机械域组件 | 同上。 |
|
||||
| `LSTP00A` | 8 | 弹性接触/端止动 | 暂不公开 | 后续机械域组件 | 依赖机械接触和事件/非光滑力模型。 |
|
||||
| `F000` | 16 | 零力源 | 暂不公开 | 后续机械边界组件 | 依赖机械域。 |
|
||||
| `FORC` | 2 | 信号转力 | 暂不公开 | 后续信号-机械跨域组件 | 依赖信号端口与机械端口。 |
|
||||
| `STEP0` | 8 | 阶跃信号源 | 暂不公开 | 后续信号源组件 | 当前公开求解器不支持信号端口仿真和事件。 |
|
||||
| `UD00` | 2 | 分段线性信号源 | 暂不公开 | 后续信号源组件 | 同上。 |
|
||||
| `F000` | 16 | 零力源 | 第一版公开 | `amesim_f000`,`mechanical` | 已作为一端机械零力边界公开,约束端口力为 0。 |
|
||||
| `FORC` | 2 | 信号转力 | 第一版公开 | `amesim_forc`,`mechanical` | 已接入信号输入 `res` 到机械端口力源,可由 `STEP0/UD00` 驱动质量组件。 |
|
||||
| `STEP0` | 8 | 阶跃信号源 | 第一版公开 | `amesim_step0`,`signals` | 已接入标量信号输出端口和求解时信号传播;当前是基础阶跃,不含更复杂事件调度语义。 |
|
||||
| `UD00` | 2 | 分段线性信号源 | 第一版公开 | `amesim_ud00`,`signals` | 已接入标量信号输出端口、8 段 start/end/t 参数、循环模式和 System XML signal connection;当前按已转换 PythonModels 语义处理最后一段外推,AMESim baseline 仍留后续复核。 |
|
||||
| `DIRECT` | 44 | 直接连接 | 不注册为组件 | System XML `Connection` | 连接不是组件;物理连接必须保持无方向端点语义。 |
|
||||
|
||||
## 建议迁移批次
|
||||
@@ -68,22 +68,25 @@
|
||||
|
||||
### 批次 2:动态管路
|
||||
|
||||
- `amesim_pnl0001`
|
||||
- `amesim_pnl0002`
|
||||
- `amesim_pnl0003`
|
||||
- `amesim_pnl0001`:已第一版公开,保留后续 baseline 精修。
|
||||
- `amesim_pnl0002`:已第一版公开,保留后续 baseline 精修。
|
||||
- `amesim_pnl0003`:已第一版公开,保留后续 baseline 精修。
|
||||
|
||||
这些模型是 `test_mql` 对齐工作的核心,公开前需要先在内部固定算例中完成 AMESim baseline 诊断闭环。不能用当前准稳态 `pipe` 替代动态管路并宣称等价。
|
||||
这些模型是 `test_mql` 对齐工作的核心。当前已先按动态组件契约公开,后续仍需要在固定算例和 AMESim baseline 上复核摩擦、换热、多方模式和大压差动态闭合;不能用准稳态 `pipe` 替代这些动态管路并宣称等价。
|
||||
|
||||
### 批次 3:信号、事件、机械和跨域组件
|
||||
|
||||
- `STEP0`、`UD00`、`PNVO001`
|
||||
- `MECMAS21`、`LMECHN1`、`LSTP00A`、`F000`、`FORC`、`PNRP17`
|
||||
- `STEP0`:已第一版公开。
|
||||
- `PNVO001`:已第一版公开,保留固定开度变体。
|
||||
- `UD00`:已第一版公开。
|
||||
- `MECMAS21`、`F000`、`FORC`:已第一版公开。
|
||||
- `LMECHN1`、`LSTP00A`、`PNRP17`
|
||||
|
||||
这些需要先设计新物理域或信号求解协议,包括端口变量、连接规则、XML 协议、前端连线兼容和最小闭合系统测试。
|
||||
当前已具备一对一标量信号传播和一维机械端口基础闭合;剩余机械/跨域组件仍需要继续设计多端机械节点、接触事件、气动-机械耦合、XML 协议、前端连线兼容和最小闭合系统测试。
|
||||
|
||||
## 下一步执行建议
|
||||
|
||||
1. 先为 AMESim 公开组件建立独立库,例如 `app.simulation.components.amesim`,不要混入 `experimental`。
|
||||
2. 先只登记一个完成度最高的气动代数组件,例如 `PNOR001` 或 `PNL00R`。
|
||||
3. 每登记一个模型,都同步补充测试和目录校验,确认 `/api/components/catalog`、System XML 编译和最小仿真都通过。
|
||||
4. 动态管路和机械/信号组件继续保留在内部 `test_mql` 路径,直到方程和求解能力补齐。
|
||||
4. 动态管路 `PNL0001/2/3`、参数化 `PNCH012`、`STEP0`、`UD00`、信号版 `PNVO001` 和机械基础件 `F000/FORC/MECMAS21` 已完成第一版公开接入;下一步转向 `LMECHN1/LSTP00A/PNRP17` 或完整实时耦合。
|
||||
@@ -21,14 +21,9 @@ class AmesimComponentMigrationMatrixTests(unittest.TestCase):
|
||||
def test_unsupported_domains_are_not_marked_as_public_candidates(self) -> None:
|
||||
text = MATRIX_PATH.read_text(encoding="utf-8")
|
||||
unsupported = {
|
||||
"F000",
|
||||
"FORC",
|
||||
"LMECHN1",
|
||||
"LSTP00A",
|
||||
"MECMAS21",
|
||||
"PNRP17",
|
||||
"STEP0",
|
||||
"UD00",
|
||||
}
|
||||
|
||||
for family in unsupported:
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from app.simulation.components.amesim.mechanical.translational import (
|
||||
AmesimF000,
|
||||
AmesimForc,
|
||||
AmesimMecmas21,
|
||||
)
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
|
||||
|
||||
|
||||
class AmesimMechanicalPublicComponentTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.medium = IdealGasMedium()
|
||||
|
||||
def test_f000_constrains_mechanical_port_force_to_zero(self) -> None:
|
||||
source = AmesimF000("zero_1")
|
||||
source.port_1.f = 12.5
|
||||
|
||||
residuals = source.pressure_flow_equation_residuals()
|
||||
|
||||
self.assertEqual(len(residuals), 1)
|
||||
self.assertEqual(residuals[0].variables, ("zero_1.port_1.f",))
|
||||
self.assertAlmostEqual(residuals[0].value, 12.5)
|
||||
|
||||
def test_forc_converts_signal_to_opposing_source_port_force(self) -> None:
|
||||
converter = AmesimForc("force_1")
|
||||
converter.res.signal = 20.0
|
||||
converter.port_2.f = -20.0
|
||||
|
||||
residuals = converter.pressure_flow_equation_residuals()
|
||||
|
||||
self.assertAlmostEqual(converter.output_force, 20.0)
|
||||
self.assertAlmostEqual(residuals[0].value, 0.0)
|
||||
self.assertEqual(converter.component_result_values(), {"force": 20.0})
|
||||
|
||||
def test_mecmas21_acceleration_uses_connected_port_forces(self) -> None:
|
||||
mass = AmesimMecmas21(
|
||||
"mass_1",
|
||||
self.medium,
|
||||
mass=2.0,
|
||||
fcoul=0.0,
|
||||
rvisc=0.0,
|
||||
wind=0.0,
|
||||
x0=0.1,
|
||||
v0=0.2,
|
||||
)
|
||||
mass.port_1.f = 10.0
|
||||
mass.port_2.f = -2.0
|
||||
|
||||
self.assertEqual(mass.get_state_vector(), [0.2, 0.1])
|
||||
self.assertAlmostEqual(mass.acceleration(), 4.0)
|
||||
self.assertEqual(mass.state_derivative_from_ports({}), [4.0, 0.2])
|
||||
self.assertAlmostEqual(mass.port_1.x, 0.1)
|
||||
self.assertAlmostEqual(mass.port_2.v, 0.2)
|
||||
|
||||
def test_mecmas21_registry_rejects_fractional_integer_options(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "useFriction must be an integer"):
|
||||
COMPONENT_MODEL_REGISTRY["amesim_mecmas21"].create(
|
||||
"mass_1",
|
||||
self.medium,
|
||||
{"useFriction": 0.5},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,166 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from app.main import (
|
||||
ReactFlowProjectPayload,
|
||||
build_reactflow_system_xml,
|
||||
compile_reactflow_network,
|
||||
run_system_xml_simulation,
|
||||
)
|
||||
from app.system_xml import validate_system_xml_document
|
||||
from tests.test_amesim_pnvo001_signal_xml import signal_edge, signal_port
|
||||
from tests.test_generic_system_xml_simulation import component_node, physical_edge
|
||||
from tests.test_system_xml_protocol import physical_port
|
||||
|
||||
|
||||
MECMAS21_DEFAULTS = {
|
||||
"mass": 2.0,
|
||||
"fstick": 0.0,
|
||||
"fcoul": 0.0,
|
||||
"rvisc": 0.0,
|
||||
"wind": 0.0,
|
||||
"dvel": 1.0e-6,
|
||||
"restdvel": 1.0e-6,
|
||||
"restcoeff": 0.65,
|
||||
"astrib": 1.0e-3,
|
||||
"xmin": -1.0,
|
||||
"Kbmin": 1.0e9,
|
||||
"Dbmin": 1.0e4,
|
||||
"Pdmin": 1.0e-4,
|
||||
"xmax": 1.0,
|
||||
"Kbmax": 1.0e9,
|
||||
"Dbmax": 1.0e4,
|
||||
"Pdmax": 1.0e-4,
|
||||
"theta": 0.0,
|
||||
"useFriction": 1.0,
|
||||
"stoptype": 4.0,
|
||||
"discContactOption": 1.0,
|
||||
"strib": 1.0,
|
||||
"frictionType": 1.0,
|
||||
"v0": 0.0,
|
||||
"x0": 0.0,
|
||||
}
|
||||
|
||||
|
||||
def mechanical_port(name: str, side: str) -> dict[str, str]:
|
||||
return physical_port(name, "bidirectional", side, domain="mechanical")
|
||||
|
||||
|
||||
def zero_force_mass_project() -> ReactFlowProjectPayload:
|
||||
return ReactFlowProjectPayload(
|
||||
name="amesim-mechanical-zero-force-smoke",
|
||||
nodes=[
|
||||
component_node("zero_left", "amesim_f000", [mechanical_port("port_1", "right")]),
|
||||
component_node(
|
||||
"mass_1",
|
||||
"amesim_mecmas21",
|
||||
[mechanical_port("port_1", "left"), mechanical_port("port_2", "right")],
|
||||
MECMAS21_DEFAULTS,
|
||||
),
|
||||
component_node("zero_right", "amesim_f000", [mechanical_port("port_1", "left")]),
|
||||
],
|
||||
edges=[
|
||||
physical_edge("edge-1", "zero_left", "port_1", "mass_1", "port_1"),
|
||||
physical_edge("edge-2", "mass_1", "port_2", "zero_right", "port_1"),
|
||||
],
|
||||
simulation={"t_start": 0.0, "t_stop": 0.02, "step": 0.01, "max_step": 0.01, "method": "BDF"},
|
||||
)
|
||||
|
||||
|
||||
def signal_force_mass_project() -> ReactFlowProjectPayload:
|
||||
parameters = dict(MECMAS21_DEFAULTS)
|
||||
parameters["mass"] = 2.0
|
||||
return ReactFlowProjectPayload(
|
||||
name="amesim-mechanical-signal-force-smoke",
|
||||
nodes=[
|
||||
component_node(
|
||||
"force_signal",
|
||||
"amesim_ud00",
|
||||
[signal_port("out", "output", "right")],
|
||||
{
|
||||
"tstart": 0.0,
|
||||
"start1": 10.0,
|
||||
"end1": 10.0,
|
||||
"t1": 1.0,
|
||||
"start2": 10.0,
|
||||
"end2": 10.0,
|
||||
"t2": 0.0,
|
||||
"start3": 10.0,
|
||||
"end3": 10.0,
|
||||
"t3": 0.0,
|
||||
"start4": 10.0,
|
||||
"end4": 10.0,
|
||||
"t4": 0.0,
|
||||
"start5": 10.0,
|
||||
"end5": 10.0,
|
||||
"t5": 0.0,
|
||||
"start6": 10.0,
|
||||
"end6": 10.0,
|
||||
"t6": 0.0,
|
||||
"start7": 10.0,
|
||||
"end7": 10.0,
|
||||
"t7": 0.0,
|
||||
"start8": 10.0,
|
||||
"end8": 10.0,
|
||||
"t8": 0.0,
|
||||
"nstages": 1.0,
|
||||
"iscyclic": 0.0,
|
||||
},
|
||||
),
|
||||
component_node(
|
||||
"force_1",
|
||||
"amesim_forc",
|
||||
[signal_port("res", "input", "left"), mechanical_port("port_2", "right")],
|
||||
),
|
||||
component_node(
|
||||
"mass_1",
|
||||
"amesim_mecmas21",
|
||||
[mechanical_port("port_1", "left"), mechanical_port("port_2", "right")],
|
||||
parameters,
|
||||
),
|
||||
component_node("zero_right", "amesim_f000", [mechanical_port("port_1", "left")]),
|
||||
],
|
||||
edges=[
|
||||
signal_edge("signal-1", "force_signal", "out", "force_1", "res"),
|
||||
physical_edge("edge-1", "force_1", "port_2", "mass_1", "port_1"),
|
||||
physical_edge("edge-2", "mass_1", "port_2", "zero_right", "port_1"),
|
||||
],
|
||||
simulation={"t_start": 0.0, "t_stop": 0.02, "step": 0.01, "max_step": 0.005, "method": "BDF"},
|
||||
)
|
||||
|
||||
|
||||
class AmesimMechanicalXmlTests(unittest.TestCase):
|
||||
def test_zero_force_mechanical_project_compiles_and_simulates(self) -> None:
|
||||
xml = build_reactflow_system_xml(zero_force_mass_project())
|
||||
report = validate_system_xml_document(xml)
|
||||
|
||||
self.assertTrue(report.valid, report.as_dict())
|
||||
network = compile_reactflow_network(zero_force_mass_project())
|
||||
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
|
||||
result = run_system_xml_simulation(xml)
|
||||
|
||||
self.assertTrue(result["success"], result["message"])
|
||||
self.assertEqual(result["series"]["time"], [0.0, 0.01, 0.02])
|
||||
self.assertEqual(result["series"]["mass_1.v"], [0.0, 0.0, 0.0])
|
||||
self.assertEqual(result["series"]["mass_1.x"], [0.0, 0.0, 0.0])
|
||||
|
||||
def test_signal_force_mechanical_project_compiles_and_simulates(self) -> None:
|
||||
xml = build_reactflow_system_xml(signal_force_mass_project())
|
||||
report = validate_system_xml_document(xml)
|
||||
|
||||
self.assertTrue(report.valid, report.as_dict())
|
||||
network = compile_reactflow_network(signal_force_mass_project())
|
||||
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
|
||||
result = run_system_xml_simulation(xml)
|
||||
|
||||
self.assertTrue(result["success"], result["message"])
|
||||
self.assertEqual(result["series"]["force_signal.out.signal"], [10.0, 10.0, 10.0])
|
||||
self.assertEqual(result["series"]["force_1.res.signal"], [10.0, 10.0, 10.0])
|
||||
self.assertAlmostEqual(result["series"]["mass_1.a"][0], 5.0)
|
||||
self.assertGreater(result["series"]["mass_1.v"][-1], 0.0)
|
||||
self.assertGreater(result["series"]["mass_1.x"][-1], 0.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,120 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from app.simulation.components.amesim.storage.chambers import AmesimPnch012
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
|
||||
|
||||
|
||||
class AmesimPnch012ComponentTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.medium = IdealGasMedium()
|
||||
|
||||
def test_default_create_preserves_parameterized_volume_contract(self) -> None:
|
||||
chamber = COMPONENT_MODEL_REGISTRY["amesim_pnch012"].create(
|
||||
"chamber_1",
|
||||
self.medium,
|
||||
{},
|
||||
)
|
||||
|
||||
self.assertIsInstance(chamber, AmesimPnch012)
|
||||
self.assertEqual(set(chamber.ports), {"port_1", "port_2", "port_3", "port_4"})
|
||||
self.assertEqual(len(chamber.get_state_vector()), 2)
|
||||
self.assertEqual(chamber.parameter_values["cvol0"], 0.015)
|
||||
self.assertAlmostEqual(chamber.total_volume(), 0.015)
|
||||
|
||||
def test_total_volume_adds_four_external_volume_parameters(self) -> None:
|
||||
chamber = AmesimPnch012(
|
||||
"chamber_1",
|
||||
self.medium,
|
||||
cvol0=0.015,
|
||||
vol1=0.001,
|
||||
vol2=0.002,
|
||||
vol3=0.003,
|
||||
vol4=0.004,
|
||||
dvol1=1.0e-6,
|
||||
dvol2=2.0e-6,
|
||||
dvol3=-1.0e-6,
|
||||
dvol4=0.0,
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(chamber.total_volume(), 0.025)
|
||||
self.assertAlmostEqual(chamber.total_volume_rate(), 2.0e-6)
|
||||
|
||||
def test_initial_state_uses_total_volume_pressure_and_temperature(self) -> None:
|
||||
chamber = AmesimPnch012(
|
||||
"chamber_1",
|
||||
self.medium,
|
||||
cvol0=0.015,
|
||||
vol1=0.010,
|
||||
p0=200000.0,
|
||||
T0=300.0,
|
||||
)
|
||||
props = chamber.properties()
|
||||
|
||||
self.assertAlmostEqual(props.p, 200000.0, delta=1.0e-8)
|
||||
self.assertAlmostEqual(props.T, 300.0)
|
||||
self.assertAlmostEqual(chamber.port_1.p, props.p)
|
||||
self.assertAlmostEqual(chamber.port_4.p, props.p)
|
||||
|
||||
def test_pressure_flow_residuals_bind_all_ports_to_chamber_state(self) -> None:
|
||||
chamber = AmesimPnch012("chamber_1", self.medium)
|
||||
chamber.properties()
|
||||
|
||||
residuals = {
|
||||
residual.id.rsplit(":", 1)[1]: residual
|
||||
for residual in chamber.pressure_flow_equation_residuals()
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
set(residuals),
|
||||
{
|
||||
"port_1_pressure_state",
|
||||
"port_2_pressure_state",
|
||||
"port_3_pressure_state",
|
||||
"port_4_pressure_state",
|
||||
},
|
||||
)
|
||||
self.assertTrue(all(abs(residual.value) < 1.0e-9 for residual in residuals.values()))
|
||||
|
||||
def test_derivative_sums_four_mass_flows_and_boundary_work(self) -> None:
|
||||
chamber = AmesimPnch012(
|
||||
"chamber_1",
|
||||
self.medium,
|
||||
dvol1=1.0e-6,
|
||||
kth=2.0,
|
||||
sth=0.5,
|
||||
extemp=310.0,
|
||||
)
|
||||
props = chamber.properties()
|
||||
chamber.port_1.m_flow = 0.2
|
||||
chamber.port_2.m_flow = -0.1
|
||||
chamber.port_3.m_flow = 0.05
|
||||
chamber.port_4.m_flow = 0.0
|
||||
|
||||
derivative = chamber.state_derivative_from_ports(
|
||||
{
|
||||
"port_1": props.h + 1000.0,
|
||||
"port_2": props.h - 1000.0,
|
||||
"port_3": props.h + 500.0,
|
||||
"port_4": props.h,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(derivative[0], 0.15)
|
||||
self.assertLess(derivative[1], 0.2 * (props.h + 1000.0) + 0.05 * (props.h + 500.0))
|
||||
|
||||
def test_results_include_total_volume_and_volume_rate(self) -> None:
|
||||
chamber = AmesimPnch012("chamber_1", self.medium, vol1=0.001, dvol1=1.0e-6)
|
||||
|
||||
values = chamber.component_result_values()
|
||||
|
||||
self.assertIn("vol", values)
|
||||
self.assertIn("dvol", values)
|
||||
self.assertAlmostEqual(values["vol"], 0.016)
|
||||
self.assertAlmostEqual(values["dvol"], 1.0e-6)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from app.main import (
|
||||
ReactFlowProjectPayload,
|
||||
build_reactflow_system_xml,
|
||||
compile_reactflow_network,
|
||||
run_system_xml_simulation,
|
||||
)
|
||||
from app.system_xml import validate_system_xml_document
|
||||
from tests.test_generic_system_xml_simulation import component_node, physical_edge
|
||||
from tests.test_system_xml_protocol import physical_port
|
||||
|
||||
|
||||
def _pnpl_node(index: int) -> object:
|
||||
return component_node(
|
||||
f"boundary_{index}",
|
||||
"amesim_pnpl01",
|
||||
[physical_port("port_1", "bidirectional", "left")],
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
def amesim_pnch012_project() -> ReactFlowProjectPayload:
|
||||
return ReactFlowProjectPayload(
|
||||
name="amesim-pnch012-smoke",
|
||||
nodes=[
|
||||
component_node(
|
||||
"chamber_1",
|
||||
"amesim_pnch012",
|
||||
[
|
||||
physical_port("port_1", "bidirectional", "left"),
|
||||
physical_port("port_2", "bidirectional", "right"),
|
||||
physical_port("port_3", "bidirectional", "left"),
|
||||
physical_port("port_4", "bidirectional", "right"),
|
||||
],
|
||||
{
|
||||
"cvol0": 0.015,
|
||||
"kth": 0.0,
|
||||
"sth": 0.7,
|
||||
"extemp": 300.0,
|
||||
"gi": 1.0,
|
||||
"p0": 200000.0,
|
||||
"T0": 300.0,
|
||||
"vol1": 0.001,
|
||||
"vol2": 0.0,
|
||||
"vol3": 0.0,
|
||||
"vol4": 0.0,
|
||||
"dvol1": 0.0,
|
||||
"dvol2": 0.0,
|
||||
"dvol3": 0.0,
|
||||
"dvol4": 0.0,
|
||||
},
|
||||
),
|
||||
_pnpl_node(1),
|
||||
_pnpl_node(2),
|
||||
_pnpl_node(3),
|
||||
_pnpl_node(4),
|
||||
],
|
||||
edges=[
|
||||
physical_edge("edge-1", "chamber_1", "port_1", "boundary_1", "port_1"),
|
||||
physical_edge("edge-2", "chamber_1", "port_2", "boundary_2", "port_1"),
|
||||
physical_edge("edge-3", "chamber_1", "port_3", "boundary_3", "port_1"),
|
||||
physical_edge("edge-4", "chamber_1", "port_4", "boundary_4", "port_1"),
|
||||
],
|
||||
simulation={"t_start": 0.0, "t_stop": 0.002, "step": 0.001, "max_step": 0.001, "method": "BDF"},
|
||||
)
|
||||
|
||||
|
||||
class AmesimPnch012XmlTests(unittest.TestCase):
|
||||
def test_pnch012_reactflow_project_compiles(self) -> None:
|
||||
network = compile_reactflow_network(amesim_pnch012_project())
|
||||
|
||||
self.assertIn("chamber_1", network.components)
|
||||
self.assertEqual(network.components["chamber_1"].model_type, "amesim_pnch012")
|
||||
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
|
||||
|
||||
def test_pnch012_system_xml_validates_and_simulates(self) -> None:
|
||||
xml = build_reactflow_system_xml(amesim_pnch012_project())
|
||||
report = validate_system_xml_document(xml)
|
||||
|
||||
self.assertTrue(report.valid, report.as_dict())
|
||||
result = run_system_xml_simulation(xml)
|
||||
|
||||
self.assertTrue(result["success"], result["message"])
|
||||
self.assertEqual(result["series"]["time"], [0.0, 0.001, 0.002])
|
||||
self.assertIn("chamber_1.port_1.m_flow", result["series"])
|
||||
self.assertIn("chamber_1.vol", result["series"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,139 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from app.simulation.components.amesim.flow.pipes import AmesimPnl0001
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
|
||||
|
||||
|
||||
class AmesimPnl0001ComponentTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.medium = IdealGasMedium()
|
||||
|
||||
def test_default_create_preserves_amesim_parameter_contract(self) -> None:
|
||||
pipe = COMPONENT_MODEL_REGISTRY["amesim_pnl0001"].create(
|
||||
"pnl_1",
|
||||
self.medium,
|
||||
{},
|
||||
)
|
||||
|
||||
self.assertIsInstance(pipe, AmesimPnl0001)
|
||||
self.assertEqual(set(pipe.ports), {"port_1", "port_2"})
|
||||
self.assertEqual(
|
||||
set(pipe.parameter_values),
|
||||
{
|
||||
"diam",
|
||||
"le",
|
||||
"rr",
|
||||
"k",
|
||||
"kth",
|
||||
"extemp",
|
||||
"gi",
|
||||
"mode",
|
||||
"p0",
|
||||
"T0",
|
||||
},
|
||||
)
|
||||
self.assertEqual(len(pipe.get_state_vector()), 2)
|
||||
self.assertAlmostEqual(pipe.volume, 7.853981633974483e-5)
|
||||
|
||||
def test_rejects_fractional_integer_parameters(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "gi must be an integer"):
|
||||
COMPONENT_MODEL_REGISTRY["amesim_pnl0001"].create(
|
||||
"pnl_1",
|
||||
self.medium,
|
||||
{"gi": 1.5},
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "mode must be an integer"):
|
||||
COMPONENT_MODEL_REGISTRY["amesim_pnl0001"].create(
|
||||
"pnl_1",
|
||||
self.medium,
|
||||
{"mode": 1.5},
|
||||
)
|
||||
|
||||
def test_initial_state_uses_pipe_volume_pressure_and_temperature(self) -> None:
|
||||
pipe = AmesimPnl0001(
|
||||
"pnl_1",
|
||||
self.medium,
|
||||
diam=0.014,
|
||||
le=1.0,
|
||||
rr=0.045 / 14.0,
|
||||
p0=15.3e6,
|
||||
T0=293.15,
|
||||
)
|
||||
props = pipe.properties()
|
||||
|
||||
self.assertAlmostEqual(pipe.volume, 1.539380400258999e-4)
|
||||
self.assertAlmostEqual(props.p, 15.3e6, delta=1.0e-5)
|
||||
self.assertAlmostEqual(props.T, 293.15)
|
||||
self.assertAlmostEqual(pipe.port_2.p, props.p)
|
||||
|
||||
def test_resistance_flow_follows_pressure_gradient(self) -> None:
|
||||
pipe = AmesimPnl0001(
|
||||
"pnl_1",
|
||||
self.medium,
|
||||
diam=0.014,
|
||||
le=1.0,
|
||||
rr=0.045 / 14.0,
|
||||
p0=15.3e6,
|
||||
T0=293.15,
|
||||
)
|
||||
props = pipe.properties()
|
||||
|
||||
forward = pipe.mass_flow(15.31e6, props.p, props.T)
|
||||
reverse = pipe.mass_flow(15.29e6, props.p, props.T)
|
||||
|
||||
self.assertGreater(forward, 0.0)
|
||||
self.assertLess(reverse, 0.0)
|
||||
self.assertAlmostEqual(abs(forward), abs(reverse), delta=abs(forward) * 0.02)
|
||||
|
||||
def test_pressure_flow_residuals_do_not_force_storage_mass_balance(self) -> None:
|
||||
pipe = AmesimPnl0001("pnl_1", self.medium)
|
||||
pipe.port_1.p = 101000.0
|
||||
pipe.port_2.m_flow = -1.0e-4
|
||||
props = pipe.properties()
|
||||
pipe.port_1.m_flow = pipe.mass_flow(pipe.port_1.p, pipe.port_2.p, props.T)
|
||||
|
||||
residuals = {
|
||||
residual.id.rsplit(":", 1)[1]: residual
|
||||
for residual in pipe.pressure_flow_equation_residuals()
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
set(residuals),
|
||||
{"port_2_pressure_state", "port_1_pressure_flow_relation"},
|
||||
)
|
||||
self.assertAlmostEqual(residuals["port_2_pressure_state"].value, 0.0)
|
||||
self.assertAlmostEqual(residuals["port_1_pressure_flow_relation"].value, 0.0)
|
||||
|
||||
def test_connection_derivative_preserves_two_port_accumulation(self) -> None:
|
||||
pipe = AmesimPnl0001("pnl_1", self.medium, kth=2.0, extemp=310.0)
|
||||
props = pipe.properties()
|
||||
pipe.port_1.m_flow = 0.2
|
||||
pipe.port_2.m_flow = -0.1
|
||||
|
||||
derivative = pipe.state_derivative_from_ports(
|
||||
{
|
||||
"port_1": props.h + 1000.0,
|
||||
"port_2": props.h - 1000.0,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(derivative[0], 0.1)
|
||||
self.assertGreater(derivative[1], 0.0)
|
||||
|
||||
def test_results_include_thermodynamic_and_pipe_diagnostics(self) -> None:
|
||||
pipe = AmesimPnl0001("pnl_1", self.medium)
|
||||
pipe.port_1.p = 101000.0
|
||||
|
||||
values = pipe.component_result_values()
|
||||
|
||||
self.assertEqual(
|
||||
set(values),
|
||||
{"m", "U", "p", "T", "rho", "u", "h", "re", "cm", "v", "ff"},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,101 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from app.main import (
|
||||
ReactFlowProjectPayload,
|
||||
build_reactflow_system_xml,
|
||||
compile_reactflow_network,
|
||||
run_system_xml_simulation,
|
||||
)
|
||||
from app.system_xml import validate_system_xml_document
|
||||
from tests.test_generic_system_xml_simulation import component_node, physical_edge
|
||||
from tests.test_system_xml_protocol import physical_port
|
||||
|
||||
|
||||
def amesim_pnl0001_project() -> ReactFlowProjectPayload:
|
||||
return ReactFlowProjectPayload(
|
||||
name="amesim-pnl0001-smoke",
|
||||
nodes=[
|
||||
component_node(
|
||||
"cylinder_1",
|
||||
"cylinder",
|
||||
[physical_port("port_b", "outlet", "right")],
|
||||
{"volume": 0.01, "p0": 500000.0, "T0": 300.0},
|
||||
),
|
||||
component_node(
|
||||
"pnl_1",
|
||||
"amesim_pnl0001",
|
||||
[
|
||||
physical_port("port_1", "bidirectional", "left"),
|
||||
physical_port("port_2", "bidirectional", "right"),
|
||||
],
|
||||
{
|
||||
"diam": 0.01,
|
||||
"le": 1.0,
|
||||
"rr": 1.0e-5,
|
||||
"k": 1.35,
|
||||
"kth": 0.0,
|
||||
"extemp": 300.0,
|
||||
"gi": 1.0,
|
||||
"mode": 2.0,
|
||||
"p0": 300000.0,
|
||||
"T0": 300.0,
|
||||
},
|
||||
),
|
||||
component_node(
|
||||
"resistance_1",
|
||||
"amesim_pnl00r",
|
||||
[
|
||||
physical_port("port_1", "bidirectional", "left"),
|
||||
physical_port("port_2", "bidirectional", "right"),
|
||||
],
|
||||
{"diam": 0.01, "le": 0.5, "rr": 1.0e-5, "gi": 1.0},
|
||||
),
|
||||
component_node(
|
||||
"tank_1",
|
||||
"tank",
|
||||
[physical_port("port_a", "inlet", "left")],
|
||||
{"volume": 0.1, "p0": 100000.0, "T0": 300.0},
|
||||
),
|
||||
],
|
||||
edges=[
|
||||
physical_edge("edge-1", "cylinder_1", "port_b", "pnl_1", "port_1"),
|
||||
physical_edge("edge-2", "pnl_1", "port_2", "resistance_1", "port_1"),
|
||||
physical_edge("edge-3", "resistance_1", "port_2", "tank_1", "port_a"),
|
||||
],
|
||||
simulation={
|
||||
"t_start": 0.0,
|
||||
"t_stop": 0.002,
|
||||
"step": 0.001,
|
||||
"max_step": 0.001,
|
||||
"method": "BDF",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class AmesimPnl0001XmlTests(unittest.TestCase):
|
||||
def test_pnl0001_reactflow_project_compiles(self) -> None:
|
||||
network = compile_reactflow_network(amesim_pnl0001_project())
|
||||
|
||||
self.assertIn("pnl_1", network.components)
|
||||
self.assertEqual(network.components["pnl_1"].model_type, "amesim_pnl0001")
|
||||
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
|
||||
|
||||
def test_pnl0001_system_xml_validates_and_simulates(self) -> None:
|
||||
xml = build_reactflow_system_xml(amesim_pnl0001_project())
|
||||
report = validate_system_xml_document(xml)
|
||||
|
||||
self.assertTrue(report.valid, report.as_dict())
|
||||
|
||||
result = run_system_xml_simulation(xml)
|
||||
|
||||
self.assertTrue(result["success"], result["message"])
|
||||
self.assertEqual(result["series"]["time"], [0.0, 0.001, 0.002])
|
||||
self.assertIn("pnl_1.port_1.m_flow", result["series"])
|
||||
self.assertIn("pnl_1.re", result["series"])
|
||||
self.assertIn("pnl_1.m", result["series"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,160 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from app.simulation.components.amesim.flow.pipes import AmesimPnl0002, AmesimPnl0003
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
|
||||
|
||||
|
||||
class AmesimPnl0002ComponentTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.medium = IdealGasMedium()
|
||||
|
||||
def test_default_create_preserves_contract(self) -> None:
|
||||
pipe = COMPONENT_MODEL_REGISTRY["amesim_pnl0002"].create("pnl_2", self.medium, {})
|
||||
|
||||
self.assertIsInstance(pipe, AmesimPnl0002)
|
||||
self.assertEqual(set(pipe.ports), {"port_1", "port_2"})
|
||||
self.assertEqual(len(pipe.get_state_vector()), 2)
|
||||
self.assertAlmostEqual(pipe.resistance_length, pipe.le / 2.0)
|
||||
|
||||
def test_center_compliance_initial_state(self) -> None:
|
||||
pipe = AmesimPnl0002(
|
||||
"pnl_2",
|
||||
self.medium,
|
||||
diam=0.02,
|
||||
le=2.0,
|
||||
rr=0.045 / 20.0,
|
||||
p0=100000.0,
|
||||
T0=293.15,
|
||||
)
|
||||
props = pipe.properties()
|
||||
|
||||
self.assertAlmostEqual(pipe.volume, 6.283185307179586e-4)
|
||||
self.assertAlmostEqual(props.p, 100000.0, delta=1.0e-6)
|
||||
self.assertAlmostEqual(props.T, 293.15)
|
||||
|
||||
def test_port_flows_enter_center_from_higher_external_pressure(self) -> None:
|
||||
pipe = AmesimPnl0002("pnl_2", self.medium, p0=100000.0, T0=293.15)
|
||||
center = pipe.properties()
|
||||
|
||||
forward = pipe.port_mass_flow(101000.0, center.p, center.T)
|
||||
reverse = pipe.port_mass_flow(99000.0, center.p, center.T)
|
||||
|
||||
self.assertGreater(forward, 0.0)
|
||||
self.assertLess(reverse, 0.0)
|
||||
|
||||
def test_pressure_flow_residuals_use_two_port_resistances(self) -> None:
|
||||
pipe = AmesimPnl0002("pnl_2", self.medium, p0=100000.0, T0=293.15)
|
||||
center = pipe.properties()
|
||||
pipe.port_1.p = 101000.0
|
||||
pipe.port_2.p = 99000.0
|
||||
pipe.port_1.m_flow = pipe.port_mass_flow(pipe.port_1.p, center.p, center.T)
|
||||
pipe.port_2.m_flow = pipe.port_mass_flow(pipe.port_2.p, center.p, center.T)
|
||||
|
||||
residuals = {
|
||||
residual.id.rsplit(":", 1)[1]: residual
|
||||
for residual in pipe.pressure_flow_equation_residuals()
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
set(residuals),
|
||||
{"port_1_pressure_flow_relation", "port_2_pressure_flow_relation"},
|
||||
)
|
||||
self.assertAlmostEqual(residuals["port_1_pressure_flow_relation"].value, 0.0)
|
||||
self.assertAlmostEqual(residuals["port_2_pressure_flow_relation"].value, 0.0)
|
||||
|
||||
def test_connection_derivative_accumulates_two_external_flows(self) -> None:
|
||||
pipe = AmesimPnl0002("pnl_2", self.medium)
|
||||
props = pipe.properties()
|
||||
pipe.port_1.m_flow = 0.2
|
||||
pipe.port_2.m_flow = -0.1
|
||||
|
||||
derivative = pipe.state_derivative_from_ports(
|
||||
{"port_1": props.h + 1000.0, "port_2": props.h - 1000.0}
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(derivative[0], 0.1)
|
||||
|
||||
|
||||
class AmesimPnl0003ComponentTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.medium = IdealGasMedium()
|
||||
|
||||
def test_default_create_preserves_contract(self) -> None:
|
||||
pipe = COMPONENT_MODEL_REGISTRY["amesim_pnl0003"].create("pnl_3", self.medium, {})
|
||||
|
||||
self.assertIsInstance(pipe, AmesimPnl0003)
|
||||
self.assertEqual(set(pipe.ports), {"port_1", "port_2"})
|
||||
self.assertEqual(len(pipe.get_state_vector()), 4)
|
||||
|
||||
def test_initial_state_uses_two_half_volume_compliances(self) -> None:
|
||||
pipe = AmesimPnl0003(
|
||||
"pnl_3",
|
||||
self.medium,
|
||||
diam=0.02,
|
||||
le=0.3,
|
||||
rr=0.045 / 20.0,
|
||||
p1_0=15.3e6,
|
||||
T1_0=293.15,
|
||||
p2_0=15.3e6,
|
||||
T2_0=293.15,
|
||||
)
|
||||
port_1 = pipe.properties_1()
|
||||
port_2 = pipe.properties_2()
|
||||
|
||||
self.assertAlmostEqual(pipe.volume, 9.424777960769381e-5)
|
||||
self.assertAlmostEqual(pipe.compliance_volume, pipe.volume / 2.0)
|
||||
self.assertAlmostEqual(port_1.p, 15.3e6, delta=1.0e-5)
|
||||
self.assertAlmostEqual(port_2.p, 15.3e6, delta=1.0e-5)
|
||||
|
||||
def test_center_resistance_flow_follows_end_pressure_gradient(self) -> None:
|
||||
pipe = AmesimPnl0003("pnl_3", self.medium, p1_0=101000.0, p2_0=100000.0)
|
||||
forward = pipe.resistance_mass_flow()
|
||||
pipe = AmesimPnl0003("pnl_3", self.medium, p1_0=100000.0, p2_0=101000.0)
|
||||
reverse = pipe.resistance_mass_flow()
|
||||
|
||||
self.assertGreater(forward, 0.0)
|
||||
self.assertLess(reverse, 0.0)
|
||||
|
||||
def test_pressure_flow_residuals_bind_both_port_pressures_to_states(self) -> None:
|
||||
pipe = AmesimPnl0003("pnl_3", self.medium)
|
||||
pipe.properties_1()
|
||||
pipe.properties_2()
|
||||
|
||||
residuals = {
|
||||
residual.id.rsplit(":", 1)[1]: residual
|
||||
for residual in pipe.pressure_flow_equation_residuals()
|
||||
}
|
||||
|
||||
self.assertEqual(set(residuals), {"port_1_pressure_state", "port_2_pressure_state"})
|
||||
self.assertAlmostEqual(residuals["port_1_pressure_state"].value, 0.0)
|
||||
self.assertAlmostEqual(residuals["port_2_pressure_state"].value, 0.0)
|
||||
|
||||
def test_connection_derivative_conserves_external_and_center_flows(self) -> None:
|
||||
pipe = AmesimPnl0003("pnl_3", self.medium, p1_0=101000.0, p2_0=100000.0)
|
||||
port_1 = pipe.properties_1()
|
||||
port_2 = pipe.properties_2()
|
||||
pipe.port_1.m_flow = 0.2
|
||||
pipe.port_2.m_flow = -0.1
|
||||
|
||||
derivative = pipe.state_derivative_from_ports(
|
||||
{"port_1": port_1.h + 1000.0, "port_2": port_2.h - 1000.0}
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(derivative[0] + derivative[2], 0.1)
|
||||
|
||||
def test_results_include_two_thermodynamic_sides_and_center_flow(self) -> None:
|
||||
pipe = AmesimPnl0003("pnl_3", self.medium, p1_0=101000.0, p2_0=100000.0)
|
||||
|
||||
values = pipe.component_result_values()
|
||||
|
||||
self.assertIn("m1", values)
|
||||
self.assertIn("m2", values)
|
||||
self.assertIn("dmctr", values)
|
||||
self.assertIn("re", values)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,159 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from app.main import (
|
||||
ReactFlowProjectPayload,
|
||||
build_reactflow_system_xml,
|
||||
compile_reactflow_network,
|
||||
run_system_xml_simulation,
|
||||
)
|
||||
from app.system_xml import validate_system_xml_document
|
||||
from tests.test_generic_system_xml_simulation import component_node, physical_edge
|
||||
from tests.test_system_xml_protocol import physical_port
|
||||
|
||||
|
||||
def _storage_nodes(*, source_pressure: float = 500000.0, sink_pressure: float = 100000.0) -> list:
|
||||
return [
|
||||
component_node(
|
||||
"cylinder_1",
|
||||
"cylinder",
|
||||
[physical_port("port_b", "outlet", "right")],
|
||||
{"volume": 0.01, "p0": source_pressure, "T0": 300.0},
|
||||
),
|
||||
component_node(
|
||||
"tank_1",
|
||||
"tank",
|
||||
[physical_port("port_a", "inlet", "left")],
|
||||
{"volume": 0.1, "p0": sink_pressure, "T0": 300.0},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def _dynamic_pipe_params() -> dict[str, float]:
|
||||
return {
|
||||
"diam": 0.01,
|
||||
"le": 1.0,
|
||||
"rr": 1.0e-5,
|
||||
"k": 1.35,
|
||||
"kth": 0.0,
|
||||
"extemp": 300.0,
|
||||
"gi": 1.0,
|
||||
"mode": 2.0,
|
||||
"p0": 300000.0,
|
||||
"T0": 300.0,
|
||||
}
|
||||
|
||||
|
||||
def _resistance_node(name: str) -> object:
|
||||
return component_node(
|
||||
name,
|
||||
"amesim_pnl00r",
|
||||
[
|
||||
physical_port("port_1", "bidirectional", "left"),
|
||||
physical_port("port_2", "bidirectional", "right"),
|
||||
],
|
||||
{"diam": 0.01, "le": 0.5, "rr": 1.0e-5, "gi": 1.0},
|
||||
)
|
||||
|
||||
|
||||
def amesim_pnl0002_project() -> ReactFlowProjectPayload:
|
||||
return ReactFlowProjectPayload(
|
||||
name="amesim-pnl0002-smoke",
|
||||
nodes=[
|
||||
_storage_nodes()[0],
|
||||
component_node(
|
||||
"pnl_2",
|
||||
"amesim_pnl0002",
|
||||
[
|
||||
physical_port("port_1", "bidirectional", "left"),
|
||||
physical_port("port_2", "bidirectional", "right"),
|
||||
],
|
||||
_dynamic_pipe_params(),
|
||||
),
|
||||
_storage_nodes()[1],
|
||||
],
|
||||
edges=[
|
||||
physical_edge("edge-1", "cylinder_1", "port_b", "pnl_2", "port_1"),
|
||||
physical_edge("edge-2", "pnl_2", "port_2", "tank_1", "port_a"),
|
||||
],
|
||||
simulation={"t_start": 0.0, "t_stop": 0.002, "step": 0.001, "max_step": 0.001, "method": "BDF"},
|
||||
)
|
||||
|
||||
|
||||
def amesim_pnl0003_project() -> ReactFlowProjectPayload:
|
||||
params = {
|
||||
key: value
|
||||
for key, value in _dynamic_pipe_params().items()
|
||||
if key not in {"p0", "T0"}
|
||||
}
|
||||
params.update({"p1_0": 300000.0, "T1_0": 300.0, "p2_0": 300000.0, "T2_0": 300.0})
|
||||
return ReactFlowProjectPayload(
|
||||
name="amesim-pnl0003-smoke",
|
||||
nodes=[
|
||||
_storage_nodes(source_pressure=300000.0, sink_pressure=300000.0)[0],
|
||||
_resistance_node("resistance_1"),
|
||||
component_node(
|
||||
"pnl_3",
|
||||
"amesim_pnl0003",
|
||||
[
|
||||
physical_port("port_1", "bidirectional", "left"),
|
||||
physical_port("port_2", "bidirectional", "right"),
|
||||
],
|
||||
params,
|
||||
),
|
||||
_resistance_node("resistance_2"),
|
||||
_storage_nodes(source_pressure=300000.0, sink_pressure=300000.0)[1],
|
||||
],
|
||||
edges=[
|
||||
physical_edge("edge-1", "cylinder_1", "port_b", "resistance_1", "port_1"),
|
||||
physical_edge("edge-2", "resistance_1", "port_2", "pnl_3", "port_1"),
|
||||
physical_edge("edge-3", "pnl_3", "port_2", "resistance_2", "port_1"),
|
||||
physical_edge("edge-4", "resistance_2", "port_2", "tank_1", "port_a"),
|
||||
],
|
||||
simulation={"t_start": 0.0, "t_stop": 0.002, "step": 0.001, "max_step": 0.001, "method": "BDF"},
|
||||
)
|
||||
|
||||
|
||||
class AmesimPnl0002Pnl0003XmlTests(unittest.TestCase):
|
||||
def test_pnl0002_reactflow_project_compiles(self) -> None:
|
||||
network = compile_reactflow_network(amesim_pnl0002_project())
|
||||
|
||||
self.assertIn("pnl_2", network.components)
|
||||
self.assertEqual(network.components["pnl_2"].model_type, "amesim_pnl0002")
|
||||
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
|
||||
|
||||
def test_pnl0002_system_xml_validates_and_simulates(self) -> None:
|
||||
xml = build_reactflow_system_xml(amesim_pnl0002_project())
|
||||
report = validate_system_xml_document(xml)
|
||||
|
||||
self.assertTrue(report.valid, report.as_dict())
|
||||
result = run_system_xml_simulation(xml)
|
||||
|
||||
self.assertTrue(result["success"], result["message"])
|
||||
self.assertEqual(result["series"]["time"], [0.0, 0.001, 0.002])
|
||||
self.assertIn("pnl_2.port_1.m_flow", result["series"])
|
||||
self.assertIn("pnl_2.m", result["series"])
|
||||
|
||||
def test_pnl0003_reactflow_project_compiles(self) -> None:
|
||||
network = compile_reactflow_network(amesim_pnl0003_project())
|
||||
|
||||
self.assertIn("pnl_3", network.components)
|
||||
self.assertEqual(network.components["pnl_3"].model_type, "amesim_pnl0003")
|
||||
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
|
||||
|
||||
def test_pnl0003_system_xml_validates_and_simulates(self) -> None:
|
||||
xml = build_reactflow_system_xml(amesim_pnl0003_project())
|
||||
report = validate_system_xml_document(xml)
|
||||
|
||||
self.assertTrue(report.valid, report.as_dict())
|
||||
result = run_system_xml_simulation(xml)
|
||||
|
||||
self.assertTrue(result["success"], result["message"])
|
||||
self.assertEqual(result["series"]["time"], [0.0, 0.001, 0.002])
|
||||
self.assertIn("pnl_3.port_1.m_flow", result["series"])
|
||||
self.assertIn("pnl_3.dmctr", result["series"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,112 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from app.main import (
|
||||
ReactFlowProjectPayload,
|
||||
build_reactflow_system_xml,
|
||||
compile_reactflow_network,
|
||||
run_system_xml_simulation,
|
||||
)
|
||||
from app.system_xml import validate_system_xml_document
|
||||
from tests.test_generic_system_xml_simulation import component_node, physical_edge
|
||||
from tests.test_system_xml_protocol import physical_port
|
||||
|
||||
|
||||
def signal_port(name: str, role: str, side: str) -> dict[str, str | None]:
|
||||
return {
|
||||
"name": name,
|
||||
"kind": "signal",
|
||||
"domain": "signal",
|
||||
"nominalRole": role,
|
||||
"positiveFlowDirection": None,
|
||||
"side": side,
|
||||
}
|
||||
|
||||
|
||||
def signal_edge(edge_id: str, source: str, source_port: str, target: str, target_port: str) -> dict[str, str]:
|
||||
return {
|
||||
"id": edge_id,
|
||||
"source": source,
|
||||
"sourceHandle": source_port,
|
||||
"target": target,
|
||||
"targetHandle": target_port,
|
||||
}
|
||||
|
||||
|
||||
def amesim_pnvo001_signal_project() -> ReactFlowProjectPayload:
|
||||
return ReactFlowProjectPayload(
|
||||
name="amesim-pnvo001-signal-smoke",
|
||||
nodes=[
|
||||
component_node(
|
||||
"step_1",
|
||||
"amesim_step0",
|
||||
[signal_port("out", "output", "right")],
|
||||
{"initial": 1.0, "final": 1.0, "time": 0.0},
|
||||
),
|
||||
component_node(
|
||||
"cylinder_1",
|
||||
"cylinder",
|
||||
[physical_port("port_b", "outlet", "right")],
|
||||
{"volume": 0.01, "p0": 300000.0, "T0": 300.0},
|
||||
),
|
||||
component_node(
|
||||
"valve_1",
|
||||
"amesim_pnvo001",
|
||||
[
|
||||
signal_port("res", "input", "left"),
|
||||
physical_port("port_2", "bidirectional", "left"),
|
||||
physical_port("port_3", "bidirectional", "right"),
|
||||
],
|
||||
{
|
||||
"cq": 0.72,
|
||||
"area0": 5.0e-6,
|
||||
"Cv": 0.5,
|
||||
"Kv": 0.4,
|
||||
"gi": 1.0,
|
||||
"flowset": 1.0,
|
||||
"opening0": 0.0,
|
||||
},
|
||||
),
|
||||
component_node(
|
||||
"tank_1",
|
||||
"tank",
|
||||
[physical_port("port_a", "inlet", "left")],
|
||||
{"volume": 0.1, "p0": 100000.0, "T0": 300.0},
|
||||
),
|
||||
],
|
||||
edges=[
|
||||
signal_edge("signal-1", "step_1", "out", "valve_1", "res"),
|
||||
physical_edge("edge-1", "cylinder_1", "port_b", "valve_1", "port_2"),
|
||||
physical_edge("edge-2", "valve_1", "port_3", "tank_1", "port_a"),
|
||||
],
|
||||
simulation={"t_start": 0.0, "t_stop": 0.002, "step": 0.001, "max_step": 0.001, "method": "BDF"},
|
||||
)
|
||||
|
||||
|
||||
class AmesimPnvo001SignalXmlTests(unittest.TestCase):
|
||||
def test_signal_project_compiles_with_signal_connection(self) -> None:
|
||||
network = compile_reactflow_network(amesim_pnvo001_signal_project())
|
||||
|
||||
self.assertIn("step_1", network.components)
|
||||
self.assertIn("valve_1", network.components)
|
||||
self.assertEqual(network.components["valve_1"].model_type, "amesim_pnvo001")
|
||||
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
|
||||
|
||||
def test_signal_system_xml_validates_and_simulates(self) -> None:
|
||||
xml = build_reactflow_system_xml(amesim_pnvo001_signal_project())
|
||||
report = validate_system_xml_document(xml)
|
||||
|
||||
self.assertTrue(report.valid, report.as_dict())
|
||||
result = run_system_xml_simulation(xml)
|
||||
|
||||
self.assertTrue(result["success"], result["message"])
|
||||
self.assertEqual(result["series"]["time"], [0.0, 0.001, 0.002])
|
||||
self.assertEqual(result["series"]["step_1.out.signal"], [1.0, 1.0, 1.0])
|
||||
self.assertEqual(result["series"]["valve_1.res.signal"], [1.0, 1.0, 1.0])
|
||||
self.assertIn("valve_1.xv", result["series"])
|
||||
self.assertGreater(result["diagnostics"]["signal"]["propagations"], 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,97 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from app.simulation.components.amesim.flow.orifices import AmesimPnvo001SignalOpening
|
||||
from app.simulation.components.amesim.signals.sources import AmesimStep0, AmesimUd00
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
|
||||
from app.simulation.solvers.signal import SignalResolver
|
||||
from app.simulation.systems.network import SimulationNetwork
|
||||
|
||||
|
||||
class AmesimSignalComponentTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.medium = IdealGasMedium()
|
||||
|
||||
def test_step0_output_switches_at_step_time(self) -> None:
|
||||
step = AmesimStep0("step_1", self.medium, initial=0.2, final=0.8, time=0.5)
|
||||
|
||||
self.assertEqual(step.output_at(0.49), 0.2)
|
||||
self.assertEqual(step.output_at(0.5), 0.8)
|
||||
self.assertEqual(step.signal_output_values(0.5), {"out": 0.8})
|
||||
|
||||
|
||||
def test_ud00_output_interpolates_piecewise_signal(self) -> None:
|
||||
signal = AmesimUd00(
|
||||
"piecewise_1",
|
||||
self.medium,
|
||||
tstart=0.5,
|
||||
starts=(0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||
ends=(10.0, 20.0, 30.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||
durations=(1.0, 2.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||
nstages=3,
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(signal.output_at(0.0), 0.0)
|
||||
self.assertAlmostEqual(signal.output_at(1.0), 5.0)
|
||||
self.assertAlmostEqual(signal.output_at(2.5), 15.0)
|
||||
self.assertAlmostEqual(signal.output_at(5.0), 35.0)
|
||||
self.assertEqual(signal.signal_output_values(2.5), {"out": 15.0})
|
||||
|
||||
def test_ud00_can_cycle_active_stages(self) -> None:
|
||||
signal = AmesimUd00(
|
||||
"piecewise_1",
|
||||
self.medium,
|
||||
starts=(0.0, 10.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||
ends=(10.0, 20.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||
durations=(1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||
nstages=2,
|
||||
iscyclic=True,
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(signal.output_at(0.25), 2.5)
|
||||
self.assertAlmostEqual(signal.output_at(1.25), 12.5)
|
||||
self.assertAlmostEqual(signal.output_at(2.25), 2.5)
|
||||
|
||||
def test_ud00_registry_rejects_fractional_stage_controls(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "nstages must be an integer"):
|
||||
COMPONENT_MODEL_REGISTRY["amesim_ud00"].create(
|
||||
"piecewise_1",
|
||||
self.medium,
|
||||
{"nstages": 1.5},
|
||||
)
|
||||
|
||||
def test_pnvo001_signal_opening_reads_res_port(self) -> None:
|
||||
valve = COMPONENT_MODEL_REGISTRY["amesim_pnvo001"].create(
|
||||
"valve_1",
|
||||
self.medium,
|
||||
{"opening0": 0.25},
|
||||
)
|
||||
|
||||
self.assertIsInstance(valve, AmesimPnvo001SignalOpening)
|
||||
self.assertEqual(set(valve.ports), {"res", "port_2", "port_3"})
|
||||
self.assertAlmostEqual(valve.opening, 0.25)
|
||||
valve.res.signal = 1.5
|
||||
self.assertAlmostEqual(valve.opening, 1.0)
|
||||
valve.res.signal = -0.5
|
||||
self.assertAlmostEqual(valve.opening, 0.0)
|
||||
|
||||
def test_signal_resolver_propagates_step_to_valve_input(self) -> None:
|
||||
network = SimulationNetwork("signal-smoke")
|
||||
step = AmesimStep0("step_1", self.medium, initial=0.0, final=0.75, time=0.1)
|
||||
valve = AmesimPnvo001SignalOpening("valve_1", self.medium, opening0=0.0)
|
||||
network.add_component(step)
|
||||
network.add_component(valve)
|
||||
network.connect("step_1", "out", "valve_1", "res", connection_id="signal-1")
|
||||
|
||||
resolver = SignalResolver(network)
|
||||
resolver.solve(0.2)
|
||||
|
||||
self.assertAlmostEqual(step.out.signal, 0.75)
|
||||
self.assertAlmostEqual(valve.res.signal, 0.75)
|
||||
self.assertAlmostEqual(valve.opening, 0.75)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,120 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from app.main import (
|
||||
ReactFlowProjectPayload,
|
||||
build_reactflow_system_xml,
|
||||
compile_reactflow_network,
|
||||
run_system_xml_simulation,
|
||||
)
|
||||
from app.system_xml import validate_system_xml_document
|
||||
from tests.test_amesim_pnvo001_signal_xml import signal_edge, signal_port
|
||||
from tests.test_generic_system_xml_simulation import component_node, physical_edge
|
||||
from tests.test_system_xml_protocol import physical_port
|
||||
|
||||
|
||||
def amesim_ud00_signal_project() -> ReactFlowProjectPayload:
|
||||
return ReactFlowProjectPayload(
|
||||
name="amesim-ud00-signal-smoke",
|
||||
nodes=[
|
||||
component_node(
|
||||
"piecewise_1",
|
||||
"amesim_ud00",
|
||||
[signal_port("out", "output", "right")],
|
||||
{
|
||||
"tstart": 0.0,
|
||||
"start1": 1.0,
|
||||
"end1": 1.0,
|
||||
"t1": 1.0,
|
||||
"start2": 1.0,
|
||||
"end2": 1.0,
|
||||
"t2": 0.0,
|
||||
"start3": 1.0,
|
||||
"end3": 1.0,
|
||||
"t3": 0.0,
|
||||
"start4": 1.0,
|
||||
"end4": 1.0,
|
||||
"t4": 0.0,
|
||||
"start5": 1.0,
|
||||
"end5": 1.0,
|
||||
"t5": 0.0,
|
||||
"start6": 1.0,
|
||||
"end6": 1.0,
|
||||
"t6": 0.0,
|
||||
"start7": 1.0,
|
||||
"end7": 1.0,
|
||||
"t7": 0.0,
|
||||
"start8": 1.0,
|
||||
"end8": 1.0,
|
||||
"t8": 0.0,
|
||||
"nstages": 1.0,
|
||||
"iscyclic": 0.0,
|
||||
},
|
||||
),
|
||||
component_node(
|
||||
"cylinder_1",
|
||||
"cylinder",
|
||||
[physical_port("port_b", "outlet", "right")],
|
||||
{"volume": 0.01, "p0": 300000.0, "T0": 300.0},
|
||||
),
|
||||
component_node(
|
||||
"valve_1",
|
||||
"amesim_pnvo001",
|
||||
[
|
||||
signal_port("res", "input", "left"),
|
||||
physical_port("port_2", "bidirectional", "left"),
|
||||
physical_port("port_3", "bidirectional", "right"),
|
||||
],
|
||||
{
|
||||
"cq": 0.72,
|
||||
"area0": 5.0e-6,
|
||||
"Cv": 0.5,
|
||||
"Kv": 0.4,
|
||||
"gi": 1.0,
|
||||
"flowset": 1.0,
|
||||
"opening0": 0.0,
|
||||
},
|
||||
),
|
||||
component_node(
|
||||
"tank_1",
|
||||
"tank",
|
||||
[physical_port("port_a", "inlet", "left")],
|
||||
{"volume": 0.1, "p0": 100000.0, "T0": 300.0},
|
||||
),
|
||||
],
|
||||
edges=[
|
||||
signal_edge("signal-1", "piecewise_1", "out", "valve_1", "res"),
|
||||
physical_edge("edge-1", "cylinder_1", "port_b", "valve_1", "port_2"),
|
||||
physical_edge("edge-2", "valve_1", "port_3", "tank_1", "port_a"),
|
||||
],
|
||||
simulation={"t_start": 0.0, "t_stop": 0.002, "step": 0.001, "max_step": 0.001, "method": "BDF"},
|
||||
)
|
||||
|
||||
|
||||
class AmesimUd00XmlTests(unittest.TestCase):
|
||||
def test_signal_project_compiles_with_signal_connection(self) -> None:
|
||||
network = compile_reactflow_network(amesim_ud00_signal_project())
|
||||
|
||||
self.assertIn("piecewise_1", network.components)
|
||||
self.assertEqual(network.components["piecewise_1"].model_type, "amesim_ud00")
|
||||
self.assertEqual(network.components["valve_1"].model_type, "amesim_pnvo001")
|
||||
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
|
||||
|
||||
def test_signal_system_xml_validates_and_simulates(self) -> None:
|
||||
xml = build_reactflow_system_xml(amesim_ud00_signal_project())
|
||||
report = validate_system_xml_document(xml)
|
||||
|
||||
self.assertTrue(report.valid, report.as_dict())
|
||||
result = run_system_xml_simulation(xml)
|
||||
|
||||
self.assertTrue(result["success"], result["message"])
|
||||
self.assertEqual(result["series"]["time"], [0.0, 0.001, 0.002])
|
||||
self.assertEqual(result["series"]["piecewise_1.out.signal"], [1.0, 1.0, 1.0])
|
||||
self.assertEqual(result["series"]["valve_1.res.signal"], [1.0, 1.0, 1.0])
|
||||
self.assertIn("valve_1.xv", result["series"])
|
||||
self.assertGreater(result["diagnostics"]["signal"]["propagations"], 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -47,8 +47,8 @@ class ComponentCatalogTests(unittest.TestCase):
|
||||
}
|
||||
|
||||
self.assertEqual(library["label"], "AMESim 组件库")
|
||||
self.assertEqual([category["id"] for category in library["categories"]], ["storage", "flow", "junctions", "boundary"])
|
||||
self.assertEqual(set(components), {"amesim_pnpl01", "amesim_pnch023", "amesim_pnor001", "amesim_pnvo001_fixed", "amesim_pnl00r", "amesim_pn3node2", "amesim_p4node2"})
|
||||
self.assertEqual([category["id"] for category in library["categories"]], ["storage", "flow", "junctions", "boundary", "signals", "mechanical"])
|
||||
self.assertEqual(set(components), {"amesim_pnpl01", "amesim_step0", "amesim_ud00", "amesim_f000", "amesim_forc", "amesim_mecmas21", "amesim_pnch023", "amesim_pnch012", "amesim_pnor001", "amesim_pnvo001_fixed", "amesim_pnvo001", "amesim_pnl00r", "amesim_pnl0001", "amesim_pnl0002", "amesim_pnl0003", "amesim_pn3node2", "amesim_p4node2"})
|
||||
self.assertEqual(
|
||||
[port["name"] for port in components["amesim_p4node2"]["ports"]],
|
||||
["port_1", "port_2", "port_3", "port_4"],
|
||||
@@ -64,6 +64,15 @@ class ComponentCatalogTests(unittest.TestCase):
|
||||
self.assertEqual(pnch_parameters["cvol"]["quantity"], "volume")
|
||||
self.assertEqual(pnch_parameters["kth"]["quantity"], "heat_transfer_coefficient")
|
||||
|
||||
pnch012_parameters = {
|
||||
parameter["name"]: parameter
|
||||
for parameter in components["amesim_pnch012"]["parameters"]
|
||||
}
|
||||
self.assertEqual(components["amesim_pnch012"]["category"]["id"], "storage")
|
||||
self.assertEqual([port["name"] for port in components["amesim_pnch012"]["ports"]], ["port_1", "port_2", "port_3", "port_4"])
|
||||
self.assertEqual(pnch012_parameters["cvol0"]["quantity"], "volume")
|
||||
self.assertEqual(pnch012_parameters["dvol1"]["unit"], "m3/s")
|
||||
|
||||
pnor_parameters = {
|
||||
parameter["name"]: parameter
|
||||
for parameter in components["amesim_pnor001"]["parameters"]
|
||||
@@ -79,6 +88,33 @@ class ComponentCatalogTests(unittest.TestCase):
|
||||
self.assertEqual(pnvo_parameters["opening"]["maximum"], 1.0)
|
||||
self.assertEqual([port["name"] for port in components["amesim_pnvo001_fixed"]["ports"]], ["port_2", "port_3"])
|
||||
|
||||
self.assertEqual(components["amesim_step0"]["category"]["id"], "signals")
|
||||
self.assertEqual([port["name"] for port in components["amesim_step0"]["ports"]], ["out"])
|
||||
ud00_parameters = {
|
||||
parameter["name"]: parameter
|
||||
for parameter in components["amesim_ud00"]["parameters"]
|
||||
}
|
||||
self.assertEqual(components["amesim_ud00"]["category"]["id"], "signals")
|
||||
self.assertEqual([port["name"] for port in components["amesim_ud00"]["ports"]], ["out"])
|
||||
self.assertEqual(ud00_parameters["tstart"]["quantity"], "time")
|
||||
self.assertEqual(ud00_parameters["nstages"]["maximum"], 8.0)
|
||||
self.assertEqual(components["amesim_f000"]["category"]["id"], "mechanical")
|
||||
self.assertEqual([port["name"] for port in components["amesim_f000"]["ports"]], ["port_1"])
|
||||
self.assertEqual(components["amesim_f000"]["ports"][0]["domain"], "mechanical")
|
||||
self.assertEqual(components["amesim_forc"]["category"]["id"], "mechanical")
|
||||
self.assertEqual([port["name"] for port in components["amesim_forc"]["ports"]], ["res", "port_2"])
|
||||
self.assertEqual(components["amesim_forc"]["ports"][1]["domain"], "mechanical")
|
||||
mecmas_parameters = {
|
||||
parameter["name"]: parameter
|
||||
for parameter in components["amesim_mecmas21"]["parameters"]
|
||||
}
|
||||
self.assertEqual(components["amesim_mecmas21"]["category"]["id"], "mechanical")
|
||||
self.assertEqual([port["name"] for port in components["amesim_mecmas21"]["ports"]], ["port_1", "port_2"])
|
||||
self.assertEqual(mecmas_parameters["mass"]["unit"], "kg")
|
||||
self.assertEqual(mecmas_parameters["Kbmin"]["unit"], "N/m")
|
||||
self.assertEqual(components["amesim_pnvo001"]["category"]["id"], "flow")
|
||||
self.assertEqual([port["name"] for port in components["amesim_pnvo001"]["ports"]], ["res", "port_2", "port_3"])
|
||||
|
||||
pnl_parameters = {
|
||||
parameter["name"]: parameter
|
||||
for parameter in components["amesim_pnl00r"]["parameters"]
|
||||
@@ -87,6 +123,23 @@ class ComponentCatalogTests(unittest.TestCase):
|
||||
self.assertEqual(pnl_parameters["diam"]["quantity"], "length")
|
||||
self.assertEqual(pnl_parameters["le"]["unit"], "m")
|
||||
|
||||
pnl0001_parameters = {
|
||||
parameter["name"]: parameter
|
||||
for parameter in components["amesim_pnl0001"]["parameters"]
|
||||
}
|
||||
self.assertEqual(components["amesim_pnl0001"]["category"]["id"], "flow")
|
||||
self.assertEqual(pnl0001_parameters["kth"]["quantity"], "heat_transfer_coefficient")
|
||||
self.assertEqual(pnl0001_parameters["p0"]["unit"], "Pa")
|
||||
|
||||
self.assertEqual(components["amesim_pnl0002"]["category"]["id"], "flow")
|
||||
self.assertEqual(components["amesim_pnl0003"]["category"]["id"], "flow")
|
||||
pnl0003_parameters = {
|
||||
parameter["name"]: parameter
|
||||
for parameter in components["amesim_pnl0003"]["parameters"]
|
||||
}
|
||||
self.assertEqual(pnl0003_parameters["p1_0"]["unit"], "Pa")
|
||||
self.assertEqual(pnl0003_parameters["T2_0"]["quantity"], "temperature")
|
||||
|
||||
def test_every_registered_component_is_in_the_catalog(self) -> None:
|
||||
self.assertEqual(
|
||||
set(self.components),
|
||||
|
||||
@@ -31,10 +31,20 @@ class ComponentRegistryTests(unittest.TestCase):
|
||||
"orifice",
|
||||
"tee",
|
||||
"amesim_pnpl01",
|
||||
"amesim_step0",
|
||||
"amesim_ud00",
|
||||
"amesim_f000",
|
||||
"amesim_forc",
|
||||
"amesim_mecmas21",
|
||||
"amesim_pnch023",
|
||||
"amesim_pnch012",
|
||||
"amesim_pnor001",
|
||||
"amesim_pnvo001_fixed",
|
||||
"amesim_pnvo001",
|
||||
"amesim_pnl00r",
|
||||
"amesim_pnl0001",
|
||||
"amesim_pnl0002",
|
||||
"amesim_pnl0003",
|
||||
"amesim_pn3node2",
|
||||
"amesim_p4node2",
|
||||
),
|
||||
@@ -51,10 +61,20 @@ class ComponentRegistryTests(unittest.TestCase):
|
||||
self.assertIn("amesim", libraries)
|
||||
self.assertEqual(libraries["amesim"], AMESIM_LIBRARY)
|
||||
self.assertEqual(models["amesim_pnpl01"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_step0"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_ud00"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_f000"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_forc"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_mecmas21"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_pnch023"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_pnch012"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_pnor001"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_pnvo001_fixed"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_pnvo001"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_pnl00r"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_pnl0001"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_pnl0002"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_pnl0003"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_pn3node2"].library.id, "amesim")
|
||||
self.assertEqual(models["amesim_p4node2"].library.id, "amesim")
|
||||
|
||||
|
||||
Reference in new issue
Block a user