接入test_mql PNVO阶跃控制与流量诊断
This commit is contained in:
1 parent
65f6d6dd87
commit
7c68caed27
7 files changed
+298
-24
No files matched your search
@@ -60,28 +60,46 @@ Python 输出只有通过 AMESim baseline 对比后才能作为数值一致性
|
|||||||
|
|
||||||
## 当前对比结果
|
## 当前对比结果
|
||||||
|
|
||||||
默认 comparison 使用 `0 -> 1e-5 s` 的短时域,目的是在数值误差扩散前检查初值、RHS 和局部耦合。
|
默认 comparison 仍使用 `0 -> 1e-5 s` 的短时域,并已新增
|
||||||
|
`dm1@pneumatic_69` 流量诊断。
|
||||||
|
|
||||||
当前已确认:
|
本轮确认原最大偏差的主因不是 PNL0001 Darcy 阻力,而是 8 个
|
||||||
|
`PNVO001` 在 Python 中被固定为 `opening=1`,忽略了各自的
|
||||||
|
`STEP0` 控制。AMESim baseline 显示 `step_8..15` 均在
|
||||||
|
`t=0.04 s` 从 0 跳到 1,对应 PNVO 在此前保持关闭。
|
||||||
|
|
||||||
- 最大绝对误差路径是 `press@pn_c1_8`。
|
接入 STEP0 控制后的默认短时对比:
|
||||||
- 初值已对齐:Python 约 `-1300 Pa`,AMESim 约 `-1300 Pa`。
|
|
||||||
- `t=1e-5 s` 时 Python 约 `-1290.75976 Pa`,AMESim 约 `-1299.98825 Pa`,末值绝对误差约 `9.22849 Pa`。
|
|
||||||
- `pn_c1_8` 的边界体积功只有约 `0.026 W`,端口焓流约 `32722 W`;当前偏差主要不像是活塞体积功导致。
|
|
||||||
|
|
||||||
这表明 132 状态仿真链路已经能够暴露真实偏差,但 PNL0001、节点流量、孔口或 PNCH012 方程仍需数值标定,尚不能声称已经达到 AMESim parity。
|
- `press@pn_c1_8` 末值误差从约 `9.22849 Pa` 降为
|
||||||
|
`0.0117249 Pa`。
|
||||||
|
- Python `dm1@pneumatic_69` 末值约
|
||||||
|
`-4.97737e-7 g/s`,AMESim 线性插值约
|
||||||
|
`-2.05425e-6 g/s`。
|
||||||
|
- canonical chamber-to-line flow 定义为
|
||||||
|
`-dm1 * 1e-3`,因此两者分别约为
|
||||||
|
`4.97737e-10 kg/s` 和 `2.05425e-9 kg/s`,绝对误差约
|
||||||
|
`1.55652e-9 kg/s`。
|
||||||
|
- AMESim 首个 `0.01 s` 区间的 `mgas1@pn_c1_8` 有限差分约
|
||||||
|
`-9.86349e-7 kg/s`;用该区间首末 `dm1` 做梯形平均得到的
|
||||||
|
chamber 质量变化率约 `-1.02713e-6 kg/s`,符号和量级一致。
|
||||||
|
|
||||||
|
当前剩余 `1e-5 s` 误差不能直接解释为物理方程误差:AMESim
|
||||||
|
baseline 只按 `0.01 s` 保存,而 comparison 在首个保存区间内做
|
||||||
|
线性插值;位移、体积和压力的初始响应并不一定是线性的。
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
下一步只围绕当前最大偏差推进:
|
1. 增加 AMESim 保存时刻对齐的诊断,优先比较事件前的
|
||||||
|
`t=0.01 s`,避免把 `1e-5 s` 线性插值误差当成物理偏差。
|
||||||
|
2. 导出 `xv@pn_morifice_1` 和 `dm2@pn_morifice_1`,检查
|
||||||
|
`t=0.04 -> 0.05 s` 开启窗口的 PNVO 流量及事件传播。
|
||||||
|
3. 在控制时序对齐后,再判断是否需要校准 PNVO 流量系数、
|
||||||
|
`pneumatic_69` 的 PNL0001 阻力或 PNCH012 能量方程。
|
||||||
|
4. 继续保留 `press@pn_c1_8`、`dm1@pneumatic_69` 和 chamber
|
||||||
|
RHS breakdown 作为同一条诊断链。
|
||||||
|
|
||||||
1. 导出 Python 的 `p4_port3_remote_chamber_to_line_flow`。
|
`pn_c1_8` 的直接主线是 `pneumatic_69`;`pneumatic_96`
|
||||||
2. 与 AMESim 的 `dm1@pneumatic_69` 比较,先做 `g/s -> kg/s` 单位转换并确认符号方向。
|
属于另一条固定气室支路,不是该诊断对象。
|
||||||
3. 比较 `t=0` 和 `t=1e-5 s`,并用 `mgas1@pn_c1_8` 的有限差分交叉验证质量导数。
|
|
||||||
4. 若流量偏差明显,检查 `pneumatic_69` 的 PNL0001 `resistance_mass_flow`、参数和 `pnnode4_16` 节点平衡。
|
|
||||||
5. 若流量吻合,再校准 PNCH012 焓流、氦气物性以及质量/能量到压力导数的换算。
|
|
||||||
|
|
||||||
`pn_c1_8` 的直接主线是 `pneumatic_69`。`pneumatic_96` 属于 `pn_node3_8 -> pn_general_chamber` 固定气室支路,不是本轮流量诊断对象。
|
|
||||||
|
|
||||||
## 运行与验证
|
## 运行与验证
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ DEFAULT_FULL_STATE_COMPARISON_DATA_PATHS = (
|
|||||||
"x1@mass_friction_endstops_18",
|
"x1@mass_friction_endstops_18",
|
||||||
"v1@mass_friction_endstops_18",
|
"v1@mass_friction_endstops_18",
|
||||||
"acc1@mass_friction_endstops_18",
|
"acc1@mass_friction_endstops_18",
|
||||||
|
"dm1@pneumatic_69",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -50,6 +51,23 @@ class TestMqlFullStateSignalDiagnostic:
|
|||||||
final_abs_error: float
|
final_abs_error: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TestMqlFullStateFlowDiagnostic:
|
||||||
|
data_path: str
|
||||||
|
initial_time_s: float
|
||||||
|
final_time_s: float
|
||||||
|
initial_python_dm1_g_s: float
|
||||||
|
initial_amesim_dm1_g_s: float
|
||||||
|
initial_python_canonical_kg_s: float
|
||||||
|
initial_amesim_canonical_kg_s: float
|
||||||
|
initial_canonical_abs_error_kg_s: float
|
||||||
|
final_python_dm1_g_s: float
|
||||||
|
final_amesim_dm1_g_s: float
|
||||||
|
final_python_canonical_kg_s: float
|
||||||
|
final_amesim_canonical_kg_s: float
|
||||||
|
final_canonical_abs_error_kg_s: float
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class TestMqlFullStateComparisonRun:
|
class TestMqlFullStateComparisonRun:
|
||||||
system: TestMqlSystem
|
system: TestMqlSystem
|
||||||
@@ -112,6 +130,35 @@ class TestMqlFullStateComparisonRun:
|
|||||||
final_abs_error=abs(final_python - final_amesim),
|
final_abs_error=abs(final_python - final_amesim),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def pnl0001_mass_flow_diagnostic(
|
||||||
|
self,
|
||||||
|
data_path: str = "dm1@pneumatic_69",
|
||||||
|
) -> TestMqlFullStateFlowDiagnostic:
|
||||||
|
diagnostic = self.signal_diagnostic(data_path)
|
||||||
|
initial_python_canonical = -diagnostic.initial_python_value * 1.0e-3
|
||||||
|
initial_amesim_canonical = -diagnostic.initial_amesim_value * 1.0e-3
|
||||||
|
final_python_canonical = -diagnostic.final_python_value * 1.0e-3
|
||||||
|
final_amesim_canonical = -diagnostic.final_amesim_value * 1.0e-3
|
||||||
|
return TestMqlFullStateFlowDiagnostic(
|
||||||
|
data_path=data_path,
|
||||||
|
initial_time_s=diagnostic.initial_time_s,
|
||||||
|
final_time_s=diagnostic.final_time_s,
|
||||||
|
initial_python_dm1_g_s=diagnostic.initial_python_value,
|
||||||
|
initial_amesim_dm1_g_s=diagnostic.initial_amesim_value,
|
||||||
|
initial_python_canonical_kg_s=initial_python_canonical,
|
||||||
|
initial_amesim_canonical_kg_s=initial_amesim_canonical,
|
||||||
|
initial_canonical_abs_error_kg_s=abs(
|
||||||
|
initial_python_canonical - initial_amesim_canonical
|
||||||
|
),
|
||||||
|
final_python_dm1_g_s=diagnostic.final_python_value,
|
||||||
|
final_amesim_dm1_g_s=diagnostic.final_amesim_value,
|
||||||
|
final_python_canonical_kg_s=final_python_canonical,
|
||||||
|
final_amesim_canonical_kg_s=final_amesim_canonical,
|
||||||
|
final_canonical_abs_error_kg_s=abs(
|
||||||
|
final_python_canonical - final_amesim_canonical
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def diagnostics_by_final_abs_error(self):
|
def diagnostics_by_final_abs_error(self):
|
||||||
return tuple(
|
return tuple(
|
||||||
sorted(
|
sorted(
|
||||||
@@ -134,6 +181,7 @@ class TestMqlFullStateComparisonRun:
|
|||||||
return self.closure.variable_chamber_rhs_diagnostic(
|
return self.closure.variable_chamber_rhs_diagnostic(
|
||||||
chamber_alias=chamber_alias,
|
chamber_alias=chamber_alias,
|
||||||
state_vector=state_vector,
|
state_vector=state_vector,
|
||||||
|
time_s=float(self.result.t[sample_index]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -298,6 +346,28 @@ def format_test_mql_full_state_comparison_summary(
|
|||||||
f"final_amesim={diagnostic.final_amesim_value}, "
|
f"final_amesim={diagnostic.final_amesim_value}, "
|
||||||
f"final_abs_error={diagnostic.final_abs_error}"
|
f"final_abs_error={diagnostic.final_abs_error}"
|
||||||
)
|
)
|
||||||
|
flow_diagnostic = _pneumatic_69_flow_diagnostic(run)
|
||||||
|
if flow_diagnostic is not None:
|
||||||
|
lines.append(
|
||||||
|
"PNL0001 canonical mass-flow diagnostic: "
|
||||||
|
f"{flow_diagnostic.data_path}"
|
||||||
|
)
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
" - convention=Python chamber-to-line flow "
|
||||||
|
"equals -AMESim dm1 * 1e-3",
|
||||||
|
f" - initial_python_kg_s="
|
||||||
|
f"{flow_diagnostic.initial_python_canonical_kg_s}",
|
||||||
|
f" - initial_amesim_kg_s="
|
||||||
|
f"{flow_diagnostic.initial_amesim_canonical_kg_s}",
|
||||||
|
f" - final_python_kg_s="
|
||||||
|
f"{flow_diagnostic.final_python_canonical_kg_s}",
|
||||||
|
f" - final_amesim_kg_s="
|
||||||
|
f"{flow_diagnostic.final_amesim_canonical_kg_s}",
|
||||||
|
f" - final_abs_error_kg_s="
|
||||||
|
f"{flow_diagnostic.final_canonical_abs_error_kg_s}",
|
||||||
|
]
|
||||||
|
)
|
||||||
chamber_diagnostic = _largest_chamber_rhs_diagnostic(run)
|
chamber_diagnostic = _largest_chamber_rhs_diagnostic(run)
|
||||||
if chamber_diagnostic is not None:
|
if chamber_diagnostic is not None:
|
||||||
lines.append(
|
lines.append(
|
||||||
@@ -319,6 +389,13 @@ def format_test_mql_full_state_comparison_summary(
|
|||||||
return "\n".join(lines) + "\n"
|
return "\n".join(lines) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _pneumatic_69_flow_diagnostic(run: TestMqlFullStateComparisonRun):
|
||||||
|
try:
|
||||||
|
return run.pnl0001_mass_flow_diagnostic()
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _largest_chamber_rhs_diagnostic(run: TestMqlFullStateComparisonRun):
|
def _largest_chamber_rhs_diagnostic(run: TestMqlFullStateComparisonRun):
|
||||||
diagnostic = run.largest_final_abs_error_diagnostic
|
diagnostic = run.largest_final_abs_error_diagnostic
|
||||||
if diagnostic is None or "@" not in diagnostic.data_path:
|
if diagnostic is None or "@" not in diagnostic.data_path:
|
||||||
|
|||||||
@@ -3908,7 +3908,9 @@ def _default_full_state_data_paths() -> tuple[str, ...]:
|
|||||||
for index in range(10, 20)
|
for index in range(10, 20)
|
||||||
for signal in ("x1", "v1", "acc1")
|
for signal in ("x1", "v1", "acc1")
|
||||||
]
|
]
|
||||||
return tuple([*chamber_paths, *piston_paths, *mass_paths])
|
return tuple(
|
||||||
|
[*chamber_paths, *piston_paths, *mass_paths, "dm1@pneumatic_69"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
_PNCH012_ALIAS_BY_SNAPSHOT_FIELD = {
|
_PNCH012_ALIAS_BY_SNAPSHOT_FIELD = {
|
||||||
@@ -3967,10 +3969,22 @@ _PRIMARY_CHAMBER_DIAGNOSTIC_FIELDS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_PNL0001_MASS_FLOW_DIAGNOSTIC_FIELDS = {
|
||||||
|
"pneumatic_69": "p4_port3_remote_chamber_to_line_flow",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestMqlFullStateClosure:
|
class TestMqlFullStateClosure:
|
||||||
def __init__(self, *, pneumatic_closure: object, mechanical_closure: object) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
pneumatic_closure: object,
|
||||||
|
mechanical_closure: object,
|
||||||
|
pneumatic_assembly: object,
|
||||||
|
) -> None:
|
||||||
self.pneumatic_closure = pneumatic_closure
|
self.pneumatic_closure = pneumatic_closure
|
||||||
self.mechanical_closure = mechanical_closure
|
self.mechanical_closure = mechanical_closure
|
||||||
|
self.pneumatic_assembly = pneumatic_assembly
|
||||||
self.pneumatic_state_count = len(pneumatic_closure.initial_state_vector())
|
self.pneumatic_state_count = len(pneumatic_closure.initial_state_vector())
|
||||||
self.mechanical_state_count = len(mechanical_closure.initial_state_vector())
|
self.mechanical_state_count = len(mechanical_closure.initial_state_vector())
|
||||||
|
|
||||||
@@ -3993,10 +4007,18 @@ class TestMqlFullStateClosure:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def snapshot(self, state_vector: list[float] | None = None) -> TestMqlFullStateSnapshot:
|
def snapshot(self, state_vector: list[float] | None = None) -> TestMqlFullStateSnapshot:
|
||||||
|
return self.snapshot_at(0.0, state_vector)
|
||||||
|
|
||||||
|
def snapshot_at(
|
||||||
|
self,
|
||||||
|
time_s: float,
|
||||||
|
state_vector: list[float] | None = None,
|
||||||
|
) -> TestMqlFullStateSnapshot:
|
||||||
values = self.initial_state_vector() if state_vector is None else list(state_vector)
|
values = self.initial_state_vector() if state_vector is None else list(state_vector)
|
||||||
pneumatic_state, mechanical_state = self._split_state(values)
|
pneumatic_state, mechanical_state = self._split_state(values)
|
||||||
mechanical_snapshot = self.mechanical_closure.snapshot(mechanical_state)
|
mechanical_snapshot = self.mechanical_closure.snapshot(mechanical_state)
|
||||||
self._sync_variable_chamber_kinematics(mechanical_snapshot)
|
self._sync_variable_chamber_kinematics(mechanical_snapshot)
|
||||||
|
self.pneumatic_assembly.set_variable_orifice_openings(time_s)
|
||||||
pneumatic_snapshot = self.pneumatic_closure.snapshot(pneumatic_state)
|
pneumatic_snapshot = self.pneumatic_closure.snapshot(pneumatic_state)
|
||||||
return TestMqlFullStateSnapshot(
|
return TestMqlFullStateSnapshot(
|
||||||
pneumatic=pneumatic_snapshot,
|
pneumatic=pneumatic_snapshot,
|
||||||
@@ -4016,6 +4038,7 @@ class TestMqlFullStateClosure:
|
|||||||
pneumatic_state, mechanical_state = self._split_state(list(state_vector))
|
pneumatic_state, mechanical_state = self._split_state(list(state_vector))
|
||||||
mechanical_snapshot = self.mechanical_closure.snapshot(mechanical_state)
|
mechanical_snapshot = self.mechanical_closure.snapshot(mechanical_state)
|
||||||
self._sync_variable_chamber_kinematics(mechanical_snapshot)
|
self._sync_variable_chamber_kinematics(mechanical_snapshot)
|
||||||
|
self.pneumatic_assembly.set_variable_orifice_openings(time_s)
|
||||||
pneumatic_snapshot = self.pneumatic_closure.snapshot(pneumatic_state)
|
pneumatic_snapshot = self.pneumatic_closure.snapshot(pneumatic_state)
|
||||||
return [
|
return [
|
||||||
*self.pneumatic_closure.rhs(pneumatic_state),
|
*self.pneumatic_closure.rhs(pneumatic_state),
|
||||||
@@ -4054,6 +4077,7 @@ class TestMqlFullStateClosure:
|
|||||||
*,
|
*,
|
||||||
chamber_alias: str,
|
chamber_alias: str,
|
||||||
state_vector: list[float],
|
state_vector: list[float],
|
||||||
|
time_s: float = 0.0,
|
||||||
) -> TestMqlVariableChamberRhsDiagnostic:
|
) -> TestMqlVariableChamberRhsDiagnostic:
|
||||||
if chamber_alias not in _PRIMARY_CHAMBER_DIAGNOSTIC_FIELDS:
|
if chamber_alias not in _PRIMARY_CHAMBER_DIAGNOSTIC_FIELDS:
|
||||||
raise KeyError(chamber_alias)
|
raise KeyError(chamber_alias)
|
||||||
@@ -4065,7 +4089,7 @@ class TestMqlFullStateClosure:
|
|||||||
for _mass_alias, piston_alias, bound_chamber_field in _PISTON_FORCE_BINDINGS
|
for _mass_alias, piston_alias, bound_chamber_field in _PISTON_FORCE_BINDINGS
|
||||||
if bound_chamber_field == chamber_field
|
if bound_chamber_field == chamber_field
|
||||||
)
|
)
|
||||||
snapshot = self.snapshot(state_vector)
|
snapshot = self.snapshot_at(time_s, state_vector)
|
||||||
chamber = getattr(self.pneumatic_closure.components, chamber_field)
|
chamber = getattr(self.pneumatic_closure.components, chamber_field)
|
||||||
chamber_properties = getattr(snapshot.pneumatic, chamber_field)
|
chamber_properties = getattr(snapshot.pneumatic, chamber_field)
|
||||||
line_properties = getattr(snapshot.pneumatic, line_field)
|
line_properties = getattr(snapshot.pneumatic, line_field)
|
||||||
@@ -4113,7 +4137,7 @@ class TestMqlFullStateClosure:
|
|||||||
from PythonModels.systems.test_mql_pneumatic import pressure_to_amesim_gauge_pa
|
from PythonModels.systems.test_mql_pneumatic import pressure_to_amesim_gauge_pa
|
||||||
|
|
||||||
selected_paths = tuple(data_paths) if data_paths is not None else _default_full_state_data_paths()
|
selected_paths = tuple(data_paths) if data_paths is not None else _default_full_state_data_paths()
|
||||||
snapshot = self.snapshot(state_vector)
|
snapshot = self.snapshot_at(time_s, state_vector)
|
||||||
rhs = self.rhs_at(time_s, state_vector)
|
rhs = self.rhs_at(time_s, state_vector)
|
||||||
chamber_properties_by_alias = {}
|
chamber_properties_by_alias = {}
|
||||||
chamber_by_alias = {}
|
chamber_by_alias = {}
|
||||||
@@ -4130,7 +4154,15 @@ class TestMqlFullStateClosure:
|
|||||||
values_by_data_path = {}
|
values_by_data_path = {}
|
||||||
for data_path in selected_paths:
|
for data_path in selected_paths:
|
||||||
signal, alias = self._split_data_path(data_path)
|
signal, alias = self._split_data_path(data_path)
|
||||||
if alias in chamber_properties_by_alias:
|
if alias in _PNL0001_MASS_FLOW_DIAGNOSTIC_FIELDS:
|
||||||
|
if signal != "dm1":
|
||||||
|
raise KeyError(data_path)
|
||||||
|
canonical_flow_kg_s = getattr(
|
||||||
|
snapshot.pneumatic,
|
||||||
|
_PNL0001_MASS_FLOW_DIAGNOSTIC_FIELDS[alias],
|
||||||
|
)
|
||||||
|
values_by_data_path[data_path] = -canonical_flow_kg_s * 1.0e3
|
||||||
|
elif alias in chamber_properties_by_alias:
|
||||||
chamber_properties = chamber_properties_by_alias[alias]
|
chamber_properties = chamber_properties_by_alias[alias]
|
||||||
chamber = chamber_by_alias[alias]
|
chamber = chamber_by_alias[alias]
|
||||||
if signal == "press":
|
if signal == "press":
|
||||||
@@ -4631,6 +4663,7 @@ class TestMqlSystem:
|
|||||||
resistance_boundary_temperature_k=resistance_boundary_temperature_k,
|
resistance_boundary_temperature_k=resistance_boundary_temperature_k,
|
||||||
),
|
),
|
||||||
mechanical_closure=self.mechanical_mass_closure(),
|
mechanical_closure=self.mechanical_mass_closure(),
|
||||||
|
pneumatic_assembly=self.pneumatic_assembly,
|
||||||
)
|
)
|
||||||
|
|
||||||
def simulate_full_state_from_spec(
|
def simulate_full_state_from_spec(
|
||||||
|
|||||||
@@ -18,12 +18,39 @@ DEFAULT_TEST_MQL_TEMPERATURE_K = 293.15
|
|||||||
DEFAULT_VARIABLE_CHAMBER_PRESSURE_BAR = 1.0
|
DEFAULT_VARIABLE_CHAMBER_PRESSURE_BAR = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TestMqlStepSignalSpec:
|
||||||
|
alias: str
|
||||||
|
initial_output: float
|
||||||
|
final_output: float
|
||||||
|
step_time_s: float
|
||||||
|
transition_duration_s: float
|
||||||
|
transition_type: int
|
||||||
|
|
||||||
|
def output_at(self, time_s: float) -> float:
|
||||||
|
if self.transition_type != 1:
|
||||||
|
raise ValueError(
|
||||||
|
f"unsupported STEP0 transition type {self.transition_type} on {self.alias}"
|
||||||
|
)
|
||||||
|
return self.initial_output if time_s < self.step_time_s else self.final_output
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TestMqlVariableOrificeControl:
|
||||||
|
orifice_alias: str
|
||||||
|
step: TestMqlStepSignalSpec
|
||||||
|
|
||||||
|
def opening_at(self, time_s: float) -> float:
|
||||||
|
return self.step.output_at(time_s)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class TestMqlPneumaticAssembly:
|
class TestMqlPneumaticAssembly:
|
||||||
fixed_chambers: dict[str, AmesimPneumaticVolume]
|
fixed_chambers: dict[str, AmesimPneumaticVolume]
|
||||||
variable_chambers: dict[str, AmesimVariablePneumaticVolume]
|
variable_chambers: dict[str, AmesimVariablePneumaticVolume]
|
||||||
fixed_orifices: dict[str, AmesimPneumaticOrifice]
|
fixed_orifices: dict[str, AmesimPneumaticOrifice]
|
||||||
variable_orifices: dict[str, AmesimPneumaticOrifice]
|
variable_orifices: dict[str, AmesimPneumaticOrifice]
|
||||||
|
variable_orifice_controls: dict[str, TestMqlVariableOrificeControl]
|
||||||
fixed_initial_absolute_pressure_pa: float
|
fixed_initial_absolute_pressure_pa: float
|
||||||
variable_initial_absolute_pressure_pa: float
|
variable_initial_absolute_pressure_pa: float
|
||||||
|
|
||||||
@@ -51,6 +78,14 @@ class TestMqlPneumaticAssembly:
|
|||||||
def component_count(self) -> int:
|
def component_count(self) -> int:
|
||||||
return self.chamber_count + self.orifice_count
|
return self.chamber_count + self.orifice_count
|
||||||
|
|
||||||
|
@property
|
||||||
|
def variable_orifice_control_count(self) -> int:
|
||||||
|
return len(self.variable_orifice_controls)
|
||||||
|
|
||||||
|
def set_variable_orifice_openings(self, time_s: float) -> None:
|
||||||
|
for alias, control in self.variable_orifice_controls.items():
|
||||||
|
self.variable_orifices[alias].opening = control.opening_at(time_s)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def aliases(self) -> tuple[str, ...]:
|
def aliases(self) -> tuple[str, ...]:
|
||||||
return tuple(
|
return tuple(
|
||||||
@@ -101,12 +136,13 @@ def build_test_mql_pneumatic_assembly(
|
|||||||
)
|
)
|
||||||
for component in config.components_by_submodel("PNOR001")
|
for component in config.components_by_submodel("PNOR001")
|
||||||
}
|
}
|
||||||
|
variable_orifice_controls = _build_variable_orifice_controls(config)
|
||||||
variable_orifices = {
|
variable_orifices = {
|
||||||
component.alias: _build_orifice(
|
component.alias: _build_orifice(
|
||||||
component,
|
component,
|
||||||
area_parameter="area0",
|
area_parameter="area0",
|
||||||
gas=gas,
|
gas=gas,
|
||||||
opening=1.0,
|
opening=variable_orifice_controls[component.alias].opening_at(0.0),
|
||||||
)
|
)
|
||||||
for component in config.components_by_submodel("PNVO001")
|
for component in config.components_by_submodel("PNVO001")
|
||||||
}
|
}
|
||||||
@@ -115,11 +151,58 @@ def build_test_mql_pneumatic_assembly(
|
|||||||
variable_chambers=variable_chambers,
|
variable_chambers=variable_chambers,
|
||||||
fixed_orifices=fixed_orifices,
|
fixed_orifices=fixed_orifices,
|
||||||
variable_orifices=variable_orifices,
|
variable_orifices=variable_orifices,
|
||||||
|
variable_orifice_controls=variable_orifice_controls,
|
||||||
fixed_initial_absolute_pressure_pa=fixed_initial_absolute_pressure_pa,
|
fixed_initial_absolute_pressure_pa=fixed_initial_absolute_pressure_pa,
|
||||||
variable_initial_absolute_pressure_pa=variable_initial_absolute_pressure_pa,
|
variable_initial_absolute_pressure_pa=variable_initial_absolute_pressure_pa,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_variable_orifice_controls(
|
||||||
|
config: TestMqlConfig,
|
||||||
|
) -> dict[str, TestMqlVariableOrificeControl]:
|
||||||
|
from PythonModels.systems.test_mql import CONNECTION_SPECS
|
||||||
|
|
||||||
|
components_by_alias = {component.alias: component for component in config.components}
|
||||||
|
variable_orifice_aliases = {
|
||||||
|
component.alias for component in config.components_by_submodel("PNVO001")
|
||||||
|
}
|
||||||
|
controls: dict[str, TestMqlVariableOrificeControl] = {}
|
||||||
|
for connection in CONNECTION_SPECS:
|
||||||
|
if connection["submodel"] != "DIRECT":
|
||||||
|
continue
|
||||||
|
source_alias = str(connection["source_component"])
|
||||||
|
target_alias = str(connection["target_component"])
|
||||||
|
if target_alias in variable_orifice_aliases:
|
||||||
|
orifice_alias = target_alias
|
||||||
|
step_alias = source_alias
|
||||||
|
elif source_alias in variable_orifice_aliases:
|
||||||
|
orifice_alias = source_alias
|
||||||
|
step_alias = target_alias
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
step_component = components_by_alias.get(step_alias)
|
||||||
|
if step_component is None or step_component.submodel != "STEP0":
|
||||||
|
continue
|
||||||
|
controls[orifice_alias] = TestMqlVariableOrificeControl(
|
||||||
|
orifice_alias=orifice_alias,
|
||||||
|
step=TestMqlStepSignalSpec(
|
||||||
|
alias=step_alias,
|
||||||
|
initial_output=step_component.parameter_value("out0"),
|
||||||
|
final_output=step_component.parameter_value("out1"),
|
||||||
|
step_time_s=step_component.parameter_value("t0"),
|
||||||
|
transition_duration_s=step_component.parameter_value("td"),
|
||||||
|
transition_type=int(step_component.parameter_value("transitionType")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
missing = variable_orifice_aliases - controls.keys()
|
||||||
|
if missing:
|
||||||
|
raise ValueError(
|
||||||
|
"missing STEP0 controls for PNVO001 components: "
|
||||||
|
+ ", ".join(sorted(missing))
|
||||||
|
)
|
||||||
|
return controls
|
||||||
|
|
||||||
|
|
||||||
def absolute_pressure_from_amesim_bar_parameter(pressure_bar: float) -> float:
|
def absolute_pressure_from_amesim_bar_parameter(pressure_bar: float) -> float:
|
||||||
return pressure_bar * BAR_TO_PA
|
return pressure_bar * BAR_TO_PA
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ class RunTestMqlFullStateComparisonScriptTests(unittest.TestCase):
|
|||||||
output_dir=output_dir,
|
output_dir=output_dir,
|
||||||
),
|
),
|
||||||
execution=TestMqlFullStateComparisonExecutionConfig(
|
execution=TestMqlFullStateComparisonExecutionConfig(
|
||||||
data_paths=("press@pn_c1_8", "vol1@pn_brp2_8"),
|
data_paths=(
|
||||||
|
"press@pn_c1_8",
|
||||||
|
"vol1@pn_brp2_8",
|
||||||
|
"dm1@pneumatic_69",
|
||||||
|
),
|
||||||
solver=SolveIVPConfig(t_start=0.0, t_stop=0.0),
|
solver=SolveIVPConfig(t_start=0.0, t_stop=0.0),
|
||||||
t_eval=(0.0,),
|
t_eval=(0.0,),
|
||||||
),
|
),
|
||||||
@@ -44,7 +48,7 @@ class RunTestMqlFullStateComparisonScriptTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(returned_output_dir, output_dir)
|
self.assertEqual(returned_output_dir, output_dir)
|
||||||
self.assertEqual(run.sample_count, 1)
|
self.assertEqual(run.sample_count, 1)
|
||||||
self.assertEqual(run.signal_count, 2)
|
self.assertEqual(run.signal_count, 3)
|
||||||
self.assertAlmostEqual(run.comparison.max_abs_error, 0.0, delta=1.0e-8)
|
self.assertAlmostEqual(run.comparison.max_abs_error, 0.0, delta=1.0e-8)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
run.metrics_by_max_abs_error()[0].data_path,
|
run.metrics_by_max_abs_error()[0].data_path,
|
||||||
@@ -59,6 +63,14 @@ class RunTestMqlFullStateComparisonScriptTests(unittest.TestCase):
|
|||||||
run.diagnostics_by_final_abs_error()[0].data_path,
|
run.diagnostics_by_final_abs_error()[0].data_path,
|
||||||
"press@pn_c1_8",
|
"press@pn_c1_8",
|
||||||
)
|
)
|
||||||
|
flow_diagnostic = run.pnl0001_mass_flow_diagnostic()
|
||||||
|
self.assertEqual(flow_diagnostic.data_path, "dm1@pneumatic_69")
|
||||||
|
self.assertAlmostEqual(flow_diagnostic.initial_python_dm1_g_s, 0.0)
|
||||||
|
self.assertAlmostEqual(flow_diagnostic.initial_amesim_dm1_g_s, 0.0)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
flow_diagnostic.initial_python_canonical_kg_s,
|
||||||
|
-flow_diagnostic.initial_python_dm1_g_s * 1.0e-3,
|
||||||
|
)
|
||||||
chamber_diagnostic = run.chamber_rhs_diagnostic("pn_c1_8")
|
chamber_diagnostic = run.chamber_rhs_diagnostic("pn_c1_8")
|
||||||
self.assertEqual(chamber_diagnostic.chamber_alias, "pn_c1_8")
|
self.assertEqual(chamber_diagnostic.chamber_alias, "pn_c1_8")
|
||||||
self.assertEqual(chamber_diagnostic.piston_alias, "pn_brp2_8")
|
self.assertEqual(chamber_diagnostic.piston_alias, "pn_brp2_8")
|
||||||
@@ -71,11 +83,16 @@ class RunTestMqlFullStateComparisonScriptTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
self.assertTrue(csv_summary_exists)
|
self.assertTrue(csv_summary_exists)
|
||||||
self.assertIn("Mode: Python 132 full-state closure comparison", summary)
|
self.assertIn("Mode: Python 132 full-state closure comparison", summary)
|
||||||
self.assertIn("Compared signals: 2", summary)
|
self.assertIn("Compared signals: 3", summary)
|
||||||
self.assertIn("Largest absolute error: press@pn_c1_8=", summary)
|
self.assertIn("Largest absolute error: press@pn_c1_8=", summary)
|
||||||
self.assertIn("Largest final endpoint error: press@pn_c1_8=", summary)
|
self.assertIn("Largest final endpoint error: press@pn_c1_8=", summary)
|
||||||
self.assertIn("Metrics by max absolute error:", summary)
|
self.assertIn("Metrics by max absolute error:", summary)
|
||||||
self.assertIn("Endpoint diagnostics by final absolute error:", summary)
|
self.assertIn("Endpoint diagnostics by final absolute error:", summary)
|
||||||
|
self.assertIn(
|
||||||
|
"PNL0001 canonical mass-flow diagnostic: dm1@pneumatic_69",
|
||||||
|
summary,
|
||||||
|
)
|
||||||
|
self.assertIn("equals -AMESim dm1 * 1e-3", summary)
|
||||||
self.assertIn("Largest endpoint chamber RHS breakdown: pn_c1_8", summary)
|
self.assertIn("Largest endpoint chamber RHS breakdown: pn_c1_8", summary)
|
||||||
self.assertIn("piston_alias=pn_brp2_8", summary)
|
self.assertIn("piston_alias=pn_brp2_8", summary)
|
||||||
self.assertIn("mass_derivative_kg_s=", summary)
|
self.assertIn("mass_derivative_kg_s=", summary)
|
||||||
@@ -84,6 +101,7 @@ class RunTestMqlFullStateComparisonScriptTests(unittest.TestCase):
|
|||||||
self.assertIn("final_amesim=", summary)
|
self.assertIn("final_amesim=", summary)
|
||||||
self.assertIn("python.press@pn_c1_8", csv_header)
|
self.assertIn("python.press@pn_c1_8", csv_header)
|
||||||
self.assertIn("amesim.vol1@pn_brp2_8", csv_header)
|
self.assertIn("amesim.vol1@pn_brp2_8", csv_header)
|
||||||
|
self.assertIn("python.dm1@pneumatic_69", csv_header)
|
||||||
|
|
||||||
def test_full_state_script_can_skip_artifact_files(self) -> None:
|
def test_full_state_script_can_skip_artifact_files(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmpdir:
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
|||||||
@@ -69,6 +69,14 @@ class TestMqlPneumaticAssemblyTests(unittest.TestCase):
|
|||||||
self.assertAlmostEqual(fixed_orifice.flow_coefficient, 0.9)
|
self.assertAlmostEqual(fixed_orifice.flow_coefficient, 0.9)
|
||||||
self.assertAlmostEqual(variable_orifice.area, 78.5e-6)
|
self.assertAlmostEqual(variable_orifice.area, 78.5e-6)
|
||||||
self.assertAlmostEqual(variable_orifice.flow_coefficient, 0.45)
|
self.assertAlmostEqual(variable_orifice.flow_coefficient, 0.45)
|
||||||
|
self.assertAlmostEqual(variable_orifice.opening, 0.0)
|
||||||
|
self.assertEqual(assembly.variable_orifice_control_count, 8)
|
||||||
|
control = assembly.variable_orifice_controls["pn_morifice_11"]
|
||||||
|
self.assertEqual(control.step.alias, "step_8")
|
||||||
|
self.assertAlmostEqual(control.opening_at(0.0), 0.0)
|
||||||
|
self.assertAlmostEqual(control.opening_at(0.039), 0.0)
|
||||||
|
self.assertAlmostEqual(control.opening_at(0.04), 1.0)
|
||||||
|
assembly.set_variable_orifice_openings(0.04)
|
||||||
self.assertAlmostEqual(variable_orifice.opening, 1.0)
|
self.assertAlmostEqual(variable_orifice.opening, 1.0)
|
||||||
|
|
||||||
def test_chamber_initial_observables_match_amesim_results(self) -> None:
|
def test_chamber_initial_observables_match_amesim_results(self) -> None:
|
||||||
|
|||||||
@@ -199,6 +199,8 @@ class TestMqlPn3NodeChamberSegmentTests(unittest.TestCase):
|
|||||||
state = closure.initial_state_vector()
|
state = closure.initial_state_vector()
|
||||||
state[4] *= 0.99
|
state[4] *= 0.99
|
||||||
state[5] *= 0.99
|
state[5] *= 0.99
|
||||||
|
self.assertAlmostEqual(closure.components.node_orifice.opening, 0.0)
|
||||||
|
closure.components.node_orifice.opening = 1.0
|
||||||
|
|
||||||
snapshot = closure.snapshot(state)
|
snapshot = closure.snapshot(state)
|
||||||
rhs = closure.rhs(state)
|
rhs = closure.rhs(state)
|
||||||
@@ -710,6 +712,39 @@ class TestMqlPn3P4NodeChamberSegmentTests(unittest.TestCase):
|
|||||||
+ diagnostic.boundary_work_w,
|
+ diagnostic.boundary_work_w,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_full_state_closure_applies_step_controls_to_variable_orifices(self) -> None:
|
||||||
|
closure = self.system.full_state_closure_from_spec(
|
||||||
|
self.spec,
|
||||||
|
inlet_node_pressure_pa=15.31e6,
|
||||||
|
resistance_boundary_pressure_pa=15.29e6,
|
||||||
|
)
|
||||||
|
state = closure.initial_state_vector()
|
||||||
|
|
||||||
|
closed_snapshot = closure.snapshot_at(0.0, state).pneumatic
|
||||||
|
open_snapshot = closure.snapshot_at(0.04, state).pneumatic
|
||||||
|
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
closed_snapshot.p4_port3_remote_node_to_primary_line_flow,
|
||||||
|
0.0,
|
||||||
|
)
|
||||||
|
self.assertGreater(
|
||||||
|
open_snapshot.p4_port3_remote_node_to_primary_line_flow,
|
||||||
|
0.0,
|
||||||
|
)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
self.system.pneumatic_assembly.variable_orifices[
|
||||||
|
"pn_morifice_1"
|
||||||
|
].opening,
|
||||||
|
1.0,
|
||||||
|
)
|
||||||
|
closure.snapshot_at(0.0, state)
|
||||||
|
self.assertAlmostEqual(
|
||||||
|
self.system.pneumatic_assembly.variable_orifices[
|
||||||
|
"pn_morifice_1"
|
||||||
|
].opening,
|
||||||
|
0.0,
|
||||||
|
)
|
||||||
|
|
||||||
def test_simulates_full_state_key_data_paths_for_amesim_comparison(self) -> None:
|
def test_simulates_full_state_key_data_paths_for_amesim_comparison(self) -> None:
|
||||||
data_paths = (
|
data_paths = (
|
||||||
"press@pn_c1_8",
|
"press@pn_c1_8",
|
||||||
@@ -1105,6 +1140,8 @@ class TestMqlPn3P4NodeChamberSegmentTests(unittest.TestCase):
|
|||||||
resistance_boundary_pressure_pa=15.29e6,
|
resistance_boundary_pressure_pa=15.29e6,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.assertAlmostEqual(closure.components.node_orifice.opening, 0.0)
|
||||||
|
closure.components.node_orifice.opening = 1.0
|
||||||
snapshot = closure.snapshot()
|
snapshot = closure.snapshot()
|
||||||
rhs = closure.rhs(closure.initial_state_vector())
|
rhs = closure.rhs(closure.initial_state_vector())
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user