公开更多 AMESim 组件并接入信号机械闭环

This commit is contained in:
huojiarong committed 2026-07-30 10:11:08 +00:00
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))
+888 -2
View File
@@ -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")
)
+7
View File
@@ -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",
}
+74
View File
@@ -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
+44 -14
View File
@@ -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
]
+62
View File
@@ -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.")
+17 -16
View File
@@ -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"]),
}
+22 -19
View File
@@ -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()
+166
View File
@@ -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()
+120
View File
@@ -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()
+93
View File
@@ -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()
+139
View File
@@ -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()
+101
View File
@@ -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()
+159
View File
@@ -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()
+112
View File
@@ -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()
+97
View File
@@ -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()
+120
View File
@@ -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()
+55 -2
View File
@@ -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),
+20
View File
@@ -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")