Files

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()