接入test_mql PNVO阶跃控制与流量诊断
This commit is contained in:
1 parent
65f6d6dd87
commit
7c68caed27
7 files changed
+298
-24
No files matched your search
@@ -34,6 +34,7 @@ DEFAULT_FULL_STATE_COMPARISON_DATA_PATHS = (
|
||||
"x1@mass_friction_endstops_18",
|
||||
"v1@mass_friction_endstops_18",
|
||||
"acc1@mass_friction_endstops_18",
|
||||
"dm1@pneumatic_69",
|
||||
)
|
||||
|
||||
|
||||
@@ -50,6 +51,23 @@ class TestMqlFullStateSignalDiagnostic:
|
||||
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)
|
||||
class TestMqlFullStateComparisonRun:
|
||||
system: TestMqlSystem
|
||||
@@ -112,6 +130,35 @@ class TestMqlFullStateComparisonRun:
|
||||
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):
|
||||
return tuple(
|
||||
sorted(
|
||||
@@ -134,6 +181,7 @@ class TestMqlFullStateComparisonRun:
|
||||
return self.closure.variable_chamber_rhs_diagnostic(
|
||||
chamber_alias=chamber_alias,
|
||||
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_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)
|
||||
if chamber_diagnostic is not None:
|
||||
lines.append(
|
||||
@@ -319,6 +389,13 @@ def format_test_mql_full_state_comparison_summary(
|
||||
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):
|
||||
diagnostic = run.largest_final_abs_error_diagnostic
|
||||
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 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 = {
|
||||
@@ -3967,10 +3969,22 @@ _PRIMARY_CHAMBER_DIAGNOSTIC_FIELDS = {
|
||||
}
|
||||
|
||||
|
||||
_PNL0001_MASS_FLOW_DIAGNOSTIC_FIELDS = {
|
||||
"pneumatic_69": "p4_port3_remote_chamber_to_line_flow",
|
||||
}
|
||||
|
||||
|
||||
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.mechanical_closure = mechanical_closure
|
||||
self.pneumatic_assembly = pneumatic_assembly
|
||||
self.pneumatic_state_count = len(pneumatic_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:
|
||||
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)
|
||||
pneumatic_state, mechanical_state = self._split_state(values)
|
||||
mechanical_snapshot = self.mechanical_closure.snapshot(mechanical_state)
|
||||
self._sync_variable_chamber_kinematics(mechanical_snapshot)
|
||||
self.pneumatic_assembly.set_variable_orifice_openings(time_s)
|
||||
pneumatic_snapshot = self.pneumatic_closure.snapshot(pneumatic_state)
|
||||
return TestMqlFullStateSnapshot(
|
||||
pneumatic=pneumatic_snapshot,
|
||||
@@ -4016,6 +4038,7 @@ class TestMqlFullStateClosure:
|
||||
pneumatic_state, mechanical_state = self._split_state(list(state_vector))
|
||||
mechanical_snapshot = self.mechanical_closure.snapshot(mechanical_state)
|
||||
self._sync_variable_chamber_kinematics(mechanical_snapshot)
|
||||
self.pneumatic_assembly.set_variable_orifice_openings(time_s)
|
||||
pneumatic_snapshot = self.pneumatic_closure.snapshot(pneumatic_state)
|
||||
return [
|
||||
*self.pneumatic_closure.rhs(pneumatic_state),
|
||||
@@ -4054,6 +4077,7 @@ class TestMqlFullStateClosure:
|
||||
*,
|
||||
chamber_alias: str,
|
||||
state_vector: list[float],
|
||||
time_s: float = 0.0,
|
||||
) -> TestMqlVariableChamberRhsDiagnostic:
|
||||
if chamber_alias not in _PRIMARY_CHAMBER_DIAGNOSTIC_FIELDS:
|
||||
raise KeyError(chamber_alias)
|
||||
@@ -4065,7 +4089,7 @@ class TestMqlFullStateClosure:
|
||||
for _mass_alias, piston_alias, bound_chamber_field in _PISTON_FORCE_BINDINGS
|
||||
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_properties = getattr(snapshot.pneumatic, chamber_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
|
||||
|
||||
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)
|
||||
chamber_properties_by_alias = {}
|
||||
chamber_by_alias = {}
|
||||
@@ -4130,7 +4154,15 @@ class TestMqlFullStateClosure:
|
||||
values_by_data_path = {}
|
||||
for data_path in selected_paths:
|
||||
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 = chamber_by_alias[alias]
|
||||
if signal == "press":
|
||||
@@ -4631,6 +4663,7 @@ class TestMqlSystem:
|
||||
resistance_boundary_temperature_k=resistance_boundary_temperature_k,
|
||||
),
|
||||
mechanical_closure=self.mechanical_mass_closure(),
|
||||
pneumatic_assembly=self.pneumatic_assembly,
|
||||
)
|
||||
|
||||
def simulate_full_state_from_spec(
|
||||
|
||||
@@ -18,12 +18,39 @@ DEFAULT_TEST_MQL_TEMPERATURE_K = 293.15
|
||||
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)
|
||||
class TestMqlPneumaticAssembly:
|
||||
fixed_chambers: dict[str, AmesimPneumaticVolume]
|
||||
variable_chambers: dict[str, AmesimVariablePneumaticVolume]
|
||||
fixed_orifices: dict[str, AmesimPneumaticOrifice]
|
||||
variable_orifices: dict[str, AmesimPneumaticOrifice]
|
||||
variable_orifice_controls: dict[str, TestMqlVariableOrificeControl]
|
||||
fixed_initial_absolute_pressure_pa: float
|
||||
variable_initial_absolute_pressure_pa: float
|
||||
|
||||
@@ -51,6 +78,14 @@ class TestMqlPneumaticAssembly:
|
||||
def component_count(self) -> int:
|
||||
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
|
||||
def aliases(self) -> tuple[str, ...]:
|
||||
return tuple(
|
||||
@@ -101,12 +136,13 @@ def build_test_mql_pneumatic_assembly(
|
||||
)
|
||||
for component in config.components_by_submodel("PNOR001")
|
||||
}
|
||||
variable_orifice_controls = _build_variable_orifice_controls(config)
|
||||
variable_orifices = {
|
||||
component.alias: _build_orifice(
|
||||
component,
|
||||
area_parameter="area0",
|
||||
gas=gas,
|
||||
opening=1.0,
|
||||
opening=variable_orifice_controls[component.alias].opening_at(0.0),
|
||||
)
|
||||
for component in config.components_by_submodel("PNVO001")
|
||||
}
|
||||
@@ -115,11 +151,58 @@ def build_test_mql_pneumatic_assembly(
|
||||
variable_chambers=variable_chambers,
|
||||
fixed_orifices=fixed_orifices,
|
||||
variable_orifices=variable_orifices,
|
||||
variable_orifice_controls=variable_orifice_controls,
|
||||
fixed_initial_absolute_pressure_pa=fixed_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:
|
||||
return pressure_bar * BAR_TO_PA
|
||||
|
||||
|
||||
Reference in new issue
Block a user