公开更多 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"]),
}