264 lines
8.6 KiB
Python
264 lines
8.6 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Mapping
|
|
|
|
from app.simulation.components.amesim.flow.pipes import AmesimPnl0002
|
|
from app.simulation.reporting.amesim_results import (
|
|
AmesimResults,
|
|
load_test_mql_amesim_results,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Pnl0002ReplayPaths:
|
|
"""AMESim data paths needed to replay one PNL0002 resistance."""
|
|
|
|
center_pressure: str
|
|
center_temperature: str
|
|
port_1_pressure: str
|
|
port_1_temperature: str
|
|
port_1_mass_flow: str
|
|
port_2_pressure: str
|
|
port_2_temperature: str
|
|
port_2_mass_flow: str
|
|
reynolds: str
|
|
friction_factor: str
|
|
|
|
|
|
PNL83_REPLAY_PATHS = Pnl0002ReplayPaths(
|
|
center_pressure="pctr@pneumatic_83",
|
|
center_temperature="tctr@pneumatic_83",
|
|
port_1_pressure="press1@pnnode4_16",
|
|
port_1_temperature="temp1@pnnode4_16",
|
|
port_1_mass_flow="dm1@pneumatic_83",
|
|
port_2_pressure="press3@pnnode4_17",
|
|
port_2_temperature="temp3@pnnode4_17",
|
|
port_2_mass_flow="dm2@pneumatic_83",
|
|
reynolds="re@pneumatic_83",
|
|
friction_factor="ff@pneumatic_83",
|
|
)
|
|
|
|
|
|
def _required_series(
|
|
results: AmesimResults,
|
|
path: str,
|
|
) -> tuple[float, ...]:
|
|
try:
|
|
values = results.series(path)
|
|
except KeyError as exc:
|
|
raise ValueError(f"AMESim replay variable is not saved: {path}") from exc
|
|
if len(values) != len(results.times):
|
|
raise ValueError(f"AMESim replay variable has an invalid length: {path}")
|
|
return values
|
|
|
|
|
|
def _metric_summary(
|
|
rows: list[dict[str, float]],
|
|
key: str,
|
|
) -> dict[str, float]:
|
|
values = [float(row[key]) for row in rows]
|
|
max_index = max(range(len(values)), key=lambda index: abs(values[index]))
|
|
return {
|
|
"maxAbs": abs(values[max_index]),
|
|
"maxAbsTime": rows[max_index]["time"],
|
|
"finalSigned": values[-1],
|
|
}
|
|
|
|
|
|
def replay_pnl0002_amesim_states(
|
|
pipe: AmesimPnl0002,
|
|
results: AmesimResults,
|
|
paths: Pnl0002ReplayPaths,
|
|
*,
|
|
amesim_mass_flow_scale: float = -1.0e-3,
|
|
) -> dict[str, object]:
|
|
"""Replay saved AMESim states through current PNL0002 flow functions.
|
|
|
|
This is a calibration-only, no-integration calculation. It does not write
|
|
states into the pipe or alter the production simulation path.
|
|
"""
|
|
|
|
series_by_field = {
|
|
field: _required_series(results, getattr(paths, field))
|
|
for field in paths.__dataclass_fields__
|
|
}
|
|
rows: list[dict[str, float]] = []
|
|
for index, time_s in enumerate(results.times):
|
|
center_pressure = series_by_field["center_pressure"][index]
|
|
center_temperature = series_by_field["center_temperature"][index]
|
|
port_1_pressure = series_by_field["port_1_pressure"][index]
|
|
port_2_pressure = series_by_field["port_2_pressure"][index]
|
|
observed_flow_1 = (
|
|
amesim_mass_flow_scale
|
|
* series_by_field["port_1_mass_flow"][index]
|
|
)
|
|
observed_flow_2 = (
|
|
amesim_mass_flow_scale
|
|
* series_by_field["port_2_mass_flow"][index]
|
|
)
|
|
upstream_temperature_1 = (
|
|
series_by_field["port_1_temperature"][index]
|
|
if observed_flow_1 >= 0.0
|
|
else center_temperature
|
|
)
|
|
upstream_temperature_2 = (
|
|
series_by_field["port_2_temperature"][index]
|
|
if observed_flow_2 >= 0.0
|
|
else center_temperature
|
|
)
|
|
predicted_flow_1 = pipe.mass_flow(
|
|
port_1_pressure,
|
|
center_pressure,
|
|
upstream_temperature_1,
|
|
)
|
|
predicted_flow_2 = pipe.mass_flow(
|
|
port_2_pressure,
|
|
center_pressure,
|
|
upstream_temperature_2,
|
|
)
|
|
reynolds_1 = pipe.reynolds_number(
|
|
observed_flow_1,
|
|
upstream_temperature_1,
|
|
)
|
|
reynolds_2 = pipe.reynolds_number(
|
|
observed_flow_2,
|
|
upstream_temperature_2,
|
|
)
|
|
friction_1 = pipe.friction_factor(reynolds_1)
|
|
friction_2 = pipe.friction_factor(reynolds_2)
|
|
replay_reynolds = 0.5 * (reynolds_1 + reynolds_2)
|
|
replay_friction = 0.5 * (friction_1 + friction_2)
|
|
rows.append(
|
|
{
|
|
"time": float(time_s),
|
|
"centerPressure": center_pressure,
|
|
"centerTemperature": center_temperature,
|
|
"port1Pressure": port_1_pressure,
|
|
"port2Pressure": port_2_pressure,
|
|
"observedPort1MassFlow": observed_flow_1,
|
|
"observedPort2MassFlow": observed_flow_2,
|
|
"predictedPort1MassFlow": predicted_flow_1,
|
|
"predictedPort2MassFlow": predicted_flow_2,
|
|
"port1MassFlowError": predicted_flow_1 - observed_flow_1,
|
|
"port2MassFlowError": predicted_flow_2 - observed_flow_2,
|
|
"amesimReynolds": series_by_field["reynolds"][index],
|
|
"replayReynolds": replay_reynolds,
|
|
"reynoldsError": (
|
|
replay_reynolds - series_by_field["reynolds"][index]
|
|
),
|
|
"amesimFrictionFactor": series_by_field["friction_factor"][index],
|
|
"replayFrictionFactor": replay_friction,
|
|
"frictionFactorError": (
|
|
replay_friction
|
|
- series_by_field["friction_factor"][index]
|
|
),
|
|
}
|
|
)
|
|
|
|
metric_keys = (
|
|
"port1MassFlowError",
|
|
"port2MassFlowError",
|
|
"reynoldsError",
|
|
"frictionFactorError",
|
|
)
|
|
return {
|
|
"mode": "amesim-state-replay-no-integration",
|
|
"component": pipe.name,
|
|
"pointCount": len(rows),
|
|
"massFlowScale": amesim_mass_flow_scale,
|
|
"paths": {
|
|
field: getattr(paths, field)
|
|
for field in paths.__dataclass_fields__
|
|
},
|
|
"parameters": dict(pipe.parameter_values),
|
|
"summary": {
|
|
key: _metric_summary(rows, key)
|
|
for key in metric_keys
|
|
},
|
|
"rows": rows,
|
|
}
|
|
|
|
|
|
def _compile_project_pipe(
|
|
project_path: Path,
|
|
component_name: str,
|
|
) -> AmesimPnl0002:
|
|
from app.main import (
|
|
ReactFlowProjectPayload,
|
|
_compile_xml_document_or_422,
|
|
_validated_xml_document_or_422,
|
|
build_reactflow_system_xml,
|
|
validate_system_xml_document,
|
|
)
|
|
|
|
payload = ReactFlowProjectPayload.model_validate_json(
|
|
project_path.read_text(encoding="utf-8")
|
|
)
|
|
xml_bytes = build_reactflow_system_xml(payload)
|
|
document = _validated_xml_document_or_422(
|
|
validate_system_xml_document(xml_bytes)
|
|
)
|
|
_project, network = _compile_xml_document_or_422(document)
|
|
try:
|
|
component = network.components[component_name]
|
|
except KeyError as exc:
|
|
raise ValueError(f"Project component does not exist: {component_name}") from exc
|
|
if not isinstance(component, AmesimPnl0002):
|
|
raise ValueError(f"Project component is not PNL0002: {component_name}")
|
|
return component
|
|
|
|
|
|
def _paths_from_arguments(arguments: argparse.Namespace) -> Pnl0002ReplayPaths:
|
|
values: Mapping[str, str] = {
|
|
field: getattr(arguments, field)
|
|
for field in PNL83_REPLAY_PATHS.__dataclass_fields__
|
|
}
|
|
return Pnl0002ReplayPaths(**values)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
description="Replay saved AMESim p/T/m_flow through a PNL0002 model without integration."
|
|
)
|
|
parser.add_argument("project", type=Path)
|
|
parser.add_argument("amesim_archive", type=Path)
|
|
parser.add_argument("output", type=Path)
|
|
parser.add_argument("--component", default="pneumatic_83")
|
|
parser.add_argument("--mass-flow-scale", type=float, default=-1.0e-3)
|
|
for field in PNL83_REPLAY_PATHS.__dataclass_fields__:
|
|
parser.add_argument(
|
|
"--" + field.replace("_", "-"),
|
|
dest=field,
|
|
default=getattr(PNL83_REPLAY_PATHS, field),
|
|
)
|
|
arguments = parser.parse_args()
|
|
|
|
pipe = _compile_project_pipe(arguments.project, arguments.component)
|
|
results = load_test_mql_amesim_results(arguments.amesim_archive)
|
|
report = replay_pnl0002_amesim_states(
|
|
pipe,
|
|
results,
|
|
_paths_from_arguments(arguments),
|
|
amesim_mass_flow_scale=arguments.mass_flow_scale,
|
|
)
|
|
arguments.output.parent.mkdir(parents=True, exist_ok=True)
|
|
arguments.output.write_text(
|
|
json.dumps(report, ensure_ascii=False, indent=2) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
print(json.dumps({key: report[key] for key in (
|
|
"mode",
|
|
"component",
|
|
"pointCount",
|
|
"parameters",
|
|
"summary",
|
|
)}, ensure_ascii=False, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|