163 lines
5.7 KiB
Python
163 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
import tarfile
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from PythonModels.systems.test_mql import CONNECTION_SPECS, GLOBAL_PARAMETERS
|
|
from PythonModels.systems.test_mql_config import resolve_numeric_expression
|
|
|
|
|
|
AMESIM_REFERENCE_PRESSURE_PA = 101_300.0
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TestMqlPnl0001Spec:
|
|
alias: str
|
|
source_component: str
|
|
source_port: str
|
|
target_component: str
|
|
target_port: str
|
|
diameter_mm: float
|
|
length_m: float
|
|
relative_roughness: float
|
|
polytropic_constant: float
|
|
heat_transfer_coefficient: float
|
|
external_temperature_k: float
|
|
gas_type_index: int
|
|
mode: int
|
|
initial_temperature_k: float
|
|
initial_gauge_pressure_pa: float
|
|
|
|
@property
|
|
def initial_absolute_pressure_pa(self) -> float:
|
|
return self.initial_gauge_pressure_pa + AMESIM_REFERENCE_PRESSURE_PA
|
|
|
|
|
|
def load_test_mql_pnl0001_specs(
|
|
archive_path: str | Path,
|
|
*,
|
|
cir_member: str = "test_mql_.cir",
|
|
) -> tuple[TestMqlPnl0001Spec, ...]:
|
|
"""Load resolved PNL0001 geometry and initial states from the AMESim source."""
|
|
with tarfile.open(archive_path) as archive:
|
|
cir_file = archive.extractfile(cir_member)
|
|
if cir_file is None:
|
|
raise ValueError(f"Missing AMESim circuit member: {cir_member}")
|
|
cir_text = cir_file.read().decode("latin1")
|
|
|
|
numeric_globals = {
|
|
name: value
|
|
for name, expression in GLOBAL_PARAMETERS.items()
|
|
if (value := resolve_numeric_expression(expression, {})) is not None
|
|
}
|
|
connections = {
|
|
str(connection["alias"]): connection
|
|
for connection in CONNECTION_SPECS
|
|
if connection["submodel"] == "PNL0001"
|
|
}
|
|
specs = []
|
|
for block in re.findall(r"<LINE>.*?</LINE>", cir_text, flags=re.DOTALL):
|
|
if _optional_text(block, "SUB_NAME") != "PNL0001":
|
|
continue
|
|
alias = _required_text(block, "ALIAS")
|
|
connection = connections.get(alias)
|
|
if connection is None:
|
|
raise ValueError(f"PNL0001 line {alias!r} is absent from CONNECTION_SPECS")
|
|
real_parameters = _parameter_expressions(block, "RPARAM")
|
|
integer_parameters = _parameter_expressions(block, "IPARAM")
|
|
state_values = _evar_values(block)
|
|
specs.append(
|
|
TestMqlPnl0001Spec(
|
|
alias=alias,
|
|
source_component=str(connection["source_component"]),
|
|
source_port=str(connection["source_port"]),
|
|
target_component=str(connection["target_component"]),
|
|
target_port=str(connection["target_port"]),
|
|
diameter_mm=_required_numeric(
|
|
alias, "diam", real_parameters, numeric_globals
|
|
),
|
|
length_m=_required_numeric(alias, "le", real_parameters, numeric_globals),
|
|
relative_roughness=_required_numeric(
|
|
alias, "rr", real_parameters, numeric_globals
|
|
),
|
|
polytropic_constant=_required_numeric(
|
|
alias, "k", real_parameters, numeric_globals
|
|
),
|
|
heat_transfer_coefficient=_required_numeric(
|
|
alias, "kth", real_parameters, numeric_globals
|
|
),
|
|
external_temperature_k=_required_numeric(
|
|
alias, "extemp", real_parameters, numeric_globals
|
|
),
|
|
gas_type_index=int(
|
|
_required_numeric(alias, "gi", integer_parameters, numeric_globals)
|
|
),
|
|
mode=int(
|
|
_required_numeric(alias, "mode", integer_parameters, numeric_globals)
|
|
),
|
|
initial_temperature_k=_required_numeric(
|
|
alias, "t2", state_values, numeric_globals
|
|
),
|
|
initial_gauge_pressure_pa=_required_numeric(
|
|
alias, "p2", state_values, numeric_globals
|
|
),
|
|
)
|
|
)
|
|
if set(connections) != {spec.alias for spec in specs}:
|
|
missing = sorted(set(connections) - {spec.alias for spec in specs})
|
|
raise ValueError(f"Missing PNL0001 parameter blocks: {missing}")
|
|
return tuple(specs)
|
|
|
|
|
|
def _parameter_expressions(block: str, tag_name: str) -> dict[str, str]:
|
|
parameters = {}
|
|
for parameter_block in re.findall(
|
|
rf"<{tag_name}>.*?</{tag_name}>",
|
|
block,
|
|
flags=re.DOTALL,
|
|
):
|
|
parameters[_required_text(parameter_block, "VARNAME")] = _required_text(
|
|
parameter_block,
|
|
"VALUE",
|
|
)
|
|
return parameters
|
|
|
|
|
|
def _evar_values(block: str) -> dict[str, str]:
|
|
values = {}
|
|
for variable_block in re.findall(r"<EVAR>.*?</EVAR>", block, flags=re.DOTALL):
|
|
value = _optional_text(variable_block, "VALUE")
|
|
if value:
|
|
values[_required_text(variable_block, "VARNAME")] = value
|
|
return values
|
|
|
|
|
|
def _required_numeric(
|
|
alias: str,
|
|
name: str,
|
|
expressions: dict[str, str],
|
|
variables: dict[str, float],
|
|
) -> float:
|
|
if name not in expressions:
|
|
raise ValueError(f"Missing {name!r} on PNL0001 line {alias!r}")
|
|
value = resolve_numeric_expression(expressions[name], variables)
|
|
if value is None:
|
|
raise ValueError(
|
|
f"Cannot resolve {name!r}={expressions[name]!r} on PNL0001 line {alias!r}"
|
|
)
|
|
return value
|
|
|
|
|
|
def _required_text(block: str, tag_name: str) -> str:
|
|
value = _optional_text(block, tag_name)
|
|
if value is None:
|
|
raise ValueError(f"Missing AMESim circuit element: {tag_name}")
|
|
return value
|
|
|
|
|
|
def _optional_text(block: str, tag_name: str) -> str | None:
|
|
match = re.search(rf"<{tag_name}>(.*?)</{tag_name}>", block, flags=re.DOTALL)
|
|
return match.group(1).strip() if match is not None else None
|