498 lines
17 KiB
Python
498 lines
17 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
from math import log10, sqrt
|
|
|
|
from app.simulation.components.amesim.flow.pipes import AmesimPnl0002, AmesimPnl0003
|
|
from app.simulation.core.medium import IdealGasMedium
|
|
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
|
|
|
|
|
|
class AmesimPnl0002ComponentTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.medium = IdealGasMedium()
|
|
|
|
def test_default_create_preserves_contract(self) -> None:
|
|
pipe = COMPONENT_MODEL_REGISTRY["amesim_pnl0002"].create("pnl_2", self.medium, {})
|
|
|
|
self.assertIsInstance(pipe, AmesimPnl0002)
|
|
self.assertEqual(set(pipe.ports), {"port_1", "port_2"})
|
|
self.assertEqual(len(pipe.get_state_vector()), 2)
|
|
self.assertAlmostEqual(pipe.resistance_length, pipe.le / 2.0)
|
|
|
|
def test_center_compliance_initial_state(self) -> None:
|
|
pipe = AmesimPnl0002(
|
|
"pnl_2",
|
|
self.medium,
|
|
diam=0.02,
|
|
le=2.0,
|
|
rr=0.045 / 20.0,
|
|
p0=100000.0,
|
|
T0=293.15,
|
|
)
|
|
props = pipe.properties()
|
|
|
|
self.assertAlmostEqual(pipe.volume, 6.283185307179586e-4)
|
|
self.assertAlmostEqual(props.p, 100000.0, delta=1.0e-6)
|
|
self.assertAlmostEqual(props.T, 293.15)
|
|
|
|
def test_port_flows_enter_center_from_higher_external_pressure(self) -> None:
|
|
pipe = AmesimPnl0002("pnl_2", self.medium, p0=100000.0, T0=293.15)
|
|
center = pipe.properties()
|
|
|
|
forward = pipe.port_mass_flow(101000.0, center.p, center.T)
|
|
reverse = pipe.port_mass_flow(99000.0, center.p, center.T)
|
|
|
|
self.assertGreater(forward, 0.0)
|
|
self.assertLess(reverse, 0.0)
|
|
|
|
def test_exact_repeated_pipe_flow_inversion_is_cached(self) -> None:
|
|
pipe = AmesimPnl0002("pnl_2", self.medium, p0=100000.0, T0=293.15)
|
|
pipe._one_way_pn2pipefr_mass_flow.cache_clear()
|
|
|
|
first = pipe.mass_flow(15.3e6, 10.0e6, 293.15)
|
|
after_first = pipe._one_way_pn2pipefr_mass_flow.cache_info()
|
|
second = pipe.mass_flow(15.3e6, 10.0e6, 293.15)
|
|
after_second = pipe._one_way_pn2pipefr_mass_flow.cache_info()
|
|
|
|
self.assertEqual(second, first)
|
|
self.assertEqual(after_first.misses, 1)
|
|
self.assertEqual(after_second.misses, 1)
|
|
self.assertEqual(after_second.hits, 1)
|
|
|
|
def test_external_upstream_flow_uses_connected_stream_temperature(self) -> None:
|
|
pipe = AmesimPnl0002("pnl_2", self.medium, p0=100000.0, T0=350.0)
|
|
center = pipe.properties()
|
|
external_pressure = 15.3e6
|
|
external_temperature = 293.15
|
|
external_h = self.medium.specific_enthalpy_at_pressure(
|
|
external_pressure,
|
|
external_temperature,
|
|
)
|
|
pipe.update_stream_outflows(
|
|
{"port_1": external_h, "port_2": center.h}
|
|
)
|
|
|
|
flow = pipe.port_mass_flow(
|
|
external_pressure,
|
|
center.p,
|
|
center.T,
|
|
port_name="port_1",
|
|
)
|
|
expected = pipe.mass_flow(
|
|
external_pressure,
|
|
center.p,
|
|
external_temperature,
|
|
)
|
|
center_temperature_flow = pipe.mass_flow(
|
|
external_pressure,
|
|
center.p,
|
|
center.T,
|
|
)
|
|
|
|
self.assertAlmostEqual(flow, expected)
|
|
self.assertNotAlmostEqual(flow, center_temperature_flow)
|
|
|
|
def test_fully_rough_limit_matches_nikuradse_colebrook_asymptote(self) -> None:
|
|
relative_roughness = 0.045 / 20.0
|
|
pipe = AmesimPnl0002(
|
|
"pnl_2",
|
|
self.medium,
|
|
diam=0.02,
|
|
le=2.0,
|
|
rr=relative_roughness,
|
|
)
|
|
expected = 1.0 / (-2.0 * log10(relative_roughness / 3.7)) ** 2
|
|
|
|
self.assertAlmostEqual(
|
|
pipe.friction_factor(1.0e12),
|
|
expected,
|
|
delta=1.0e-10,
|
|
)
|
|
|
|
def test_missing_stream_cache_preserves_center_temperature_fallback(self) -> None:
|
|
pipe = AmesimPnl0002("pnl_2", self.medium, p0=100000.0, T0=350.0)
|
|
center = pipe.properties()
|
|
external_pressure = 15.3e6
|
|
|
|
flow = pipe.port_mass_flow(
|
|
external_pressure,
|
|
center.p,
|
|
center.T,
|
|
port_name="port_1",
|
|
)
|
|
|
|
self.assertAlmostEqual(
|
|
flow,
|
|
pipe.mass_flow(external_pressure, center.p, center.T),
|
|
)
|
|
|
|
def test_center_upstream_flow_keeps_center_temperature(self) -> None:
|
|
pipe = AmesimPnl0002("pnl_2", self.medium, p0=15.3e6, T0=350.0)
|
|
center = pipe.properties()
|
|
external_pressure = 1.0e5
|
|
external_h = self.medium.specific_enthalpy_at_pressure(
|
|
external_pressure,
|
|
293.15,
|
|
)
|
|
pipe.update_stream_outflows(
|
|
{"port_1": external_h, "port_2": center.h}
|
|
)
|
|
|
|
flow = pipe.port_mass_flow(
|
|
external_pressure,
|
|
center.p,
|
|
center.T,
|
|
port_name="port_1",
|
|
)
|
|
|
|
self.assertAlmostEqual(
|
|
flow,
|
|
pipe.mass_flow(external_pressure, center.p, center.T),
|
|
)
|
|
|
|
def test_pressure_flow_residuals_use_two_port_resistances(self) -> None:
|
|
pipe = AmesimPnl0002("pnl_2", self.medium, p0=100000.0, T0=293.15)
|
|
center = pipe.properties()
|
|
pipe.port_1.p = 101000.0
|
|
pipe.port_2.p = 99000.0
|
|
pipe.port_1.m_flow = pipe.port_mass_flow(pipe.port_1.p, center.p, center.T)
|
|
pipe.port_2.m_flow = pipe.port_mass_flow(pipe.port_2.p, center.p, center.T)
|
|
|
|
residuals = {
|
|
residual.id.rsplit(":", 1)[1]: residual
|
|
for residual in pipe.pressure_flow_equation_residuals()
|
|
}
|
|
|
|
self.assertEqual(
|
|
set(residuals),
|
|
{"port_1_pressure_flow_relation", "port_2_pressure_flow_relation"},
|
|
)
|
|
self.assertAlmostEqual(residuals["port_1_pressure_flow_relation"].value, 0.0)
|
|
self.assertAlmostEqual(residuals["port_2_pressure_flow_relation"].value, 0.0)
|
|
|
|
def test_connection_derivative_accumulates_two_external_flows(self) -> None:
|
|
pipe = AmesimPnl0002("pnl_2", self.medium)
|
|
props = pipe.properties()
|
|
pipe.port_1.m_flow = 0.2
|
|
pipe.port_2.m_flow = -0.1
|
|
|
|
derivative = pipe.state_derivative_from_ports(
|
|
{"port_1": props.h + 1000.0, "port_2": props.h - 1000.0}
|
|
)
|
|
|
|
self.assertAlmostEqual(derivative[0], 0.1)
|
|
|
|
def test_connection_derivative_uses_energy_balanced_node_enthalpy(self) -> None:
|
|
pipe = AmesimPnl0002("pnl_2", self.medium)
|
|
props = pipe.properties()
|
|
pipe.port_1.m_flow = 0.2
|
|
pipe.port_2.m_flow = 0.1
|
|
reference_h = props.h + 10_000.0
|
|
pipe.update_flow_temperature_references(
|
|
{"port_1": reference_h, "port_2": reference_h}
|
|
)
|
|
|
|
derivative = pipe.state_derivative_from_ports(
|
|
{"port_1": props.h, "port_2": props.h}
|
|
)
|
|
|
|
self.assertAlmostEqual(derivative[0], 0.3)
|
|
self.assertAlmostEqual(derivative[1], 0.3 * props.h)
|
|
|
|
def test_result_cm_averages_bare_parameter_for_each_resistance(self) -> None:
|
|
pipe = AmesimPnl0002(
|
|
"pnl_2",
|
|
self.medium,
|
|
diam=0.02,
|
|
le=2.0,
|
|
rr=0.045 / 20.0,
|
|
p0=100000.0,
|
|
T0=350.0,
|
|
)
|
|
center = pipe.properties()
|
|
pipe.port_1.p = 130000.0
|
|
pipe.port_2.p = 80000.0
|
|
pipe.update_flow_temperature_references(
|
|
{
|
|
"port_1": self.medium.specific_enthalpy_at_pressure(
|
|
pipe.port_1.p,
|
|
250.0,
|
|
),
|
|
"port_2": self.medium.specific_enthalpy_at_pressure(
|
|
pipe.port_2.p,
|
|
450.0,
|
|
),
|
|
}
|
|
)
|
|
|
|
raw_parameters: list[float] = []
|
|
bare_parameters: list[float] = []
|
|
frictions: list[float] = []
|
|
for port_name, port in (
|
|
("port_1", pipe.port_1),
|
|
("port_2", pipe.port_2),
|
|
):
|
|
flow = pipe.port_mass_flow(
|
|
port.p,
|
|
center.p,
|
|
center.T,
|
|
port_name=port_name,
|
|
)
|
|
if flow >= 0.0:
|
|
upstream_pressure = port.p
|
|
upstream_temperature = pipe.medium.temperature_from_pressure_enthalpy(
|
|
port.p,
|
|
pipe._connected_h[port_name],
|
|
)
|
|
else:
|
|
upstream_pressure = center.p
|
|
upstream_temperature = center.T
|
|
reynolds = pipe.reynolds_number(flow, upstream_temperature)
|
|
friction = pipe.friction_factor(reynolds)
|
|
raw = (
|
|
abs(flow)
|
|
* sqrt(upstream_temperature)
|
|
/ (pipe.area * upstream_pressure)
|
|
)
|
|
flow_coefficient = sqrt(
|
|
pipe.diam / (pipe.resistance_length * friction)
|
|
)
|
|
raw_parameters.append(raw)
|
|
bare_parameters.append(raw / flow_coefficient)
|
|
frictions.append(friction)
|
|
|
|
expected = sum(bare_parameters) / 2.0
|
|
collapsed = (sum(raw_parameters) / 2.0) / sqrt(
|
|
pipe.diam
|
|
/ (pipe.resistance_length * (sum(frictions) / 2.0))
|
|
)
|
|
|
|
actual = pipe.component_result_values()["cm"]
|
|
|
|
self.assertAlmostEqual(actual, expected)
|
|
self.assertGreater(abs(actual - collapsed), 1.0e-8)
|
|
|
|
|
|
class AmesimPnl0003ComponentTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.medium = IdealGasMedium()
|
|
|
|
def test_default_create_preserves_contract(self) -> None:
|
|
pipe = COMPONENT_MODEL_REGISTRY["amesim_pnl0003"].create("pnl_3", self.medium, {})
|
|
|
|
self.assertIsInstance(pipe, AmesimPnl0003)
|
|
self.assertEqual(set(pipe.ports), {"port_1", "port_2"})
|
|
self.assertEqual(len(pipe.get_state_vector()), 4)
|
|
|
|
def test_initial_state_uses_two_half_volume_compliances(self) -> None:
|
|
pipe = AmesimPnl0003(
|
|
"pnl_3",
|
|
self.medium,
|
|
diam=0.02,
|
|
le=0.3,
|
|
rr=0.045 / 20.0,
|
|
p1_0=15.3e6,
|
|
T1_0=293.15,
|
|
p2_0=15.3e6,
|
|
T2_0=293.15,
|
|
)
|
|
port_1 = pipe.properties_1()
|
|
port_2 = pipe.properties_2()
|
|
|
|
self.assertAlmostEqual(pipe.volume, 9.424777960769381e-5)
|
|
self.assertAlmostEqual(pipe.compliance_volume, pipe.volume / 2.0)
|
|
self.assertAlmostEqual(port_1.p, 15.3e6, delta=1.0e-5)
|
|
self.assertAlmostEqual(port_2.p, 15.3e6, delta=1.0e-5)
|
|
|
|
def test_center_resistance_flow_follows_end_pressure_gradient(self) -> None:
|
|
pipe = AmesimPnl0003("pnl_3", self.medium, p1_0=101000.0, p2_0=100000.0)
|
|
forward = pipe.resistance_mass_flow()
|
|
pipe = AmesimPnl0003("pnl_3", self.medium, p1_0=100000.0, p2_0=101000.0)
|
|
reverse = pipe.resistance_mass_flow()
|
|
|
|
self.assertGreater(forward, 0.0)
|
|
self.assertLess(reverse, 0.0)
|
|
|
|
def test_center_flow_is_continuous_across_former_relative_deadband(self) -> None:
|
|
base_pressure = 8.0e6
|
|
former_threshold = base_pressure * 1.0e-7
|
|
|
|
below = AmesimPnl0003(
|
|
"below",
|
|
self.medium,
|
|
p1_0=base_pressure + 0.99 * former_threshold,
|
|
p2_0=base_pressure,
|
|
).resistance_mass_flow()
|
|
above = AmesimPnl0003(
|
|
"above",
|
|
self.medium,
|
|
p1_0=base_pressure + 1.01 * former_threshold,
|
|
p2_0=base_pressure,
|
|
).resistance_mass_flow()
|
|
reverse = AmesimPnl0003(
|
|
"reverse",
|
|
self.medium,
|
|
p1_0=base_pressure,
|
|
p2_0=base_pressure + 0.99 * former_threshold,
|
|
).resistance_mass_flow()
|
|
equal = AmesimPnl0003(
|
|
"equal",
|
|
self.medium,
|
|
p1_0=base_pressure,
|
|
p2_0=base_pressure,
|
|
).resistance_mass_flow()
|
|
small = AmesimPnl0003(
|
|
"small",
|
|
self.medium,
|
|
p1_0=15.3e6 + 0.1,
|
|
p2_0=15.3e6,
|
|
).resistance_mass_flow()
|
|
|
|
self.assertGreater(below, 0.0)
|
|
self.assertGreater(above, below)
|
|
self.assertLess(reverse, 0.0)
|
|
self.assertAlmostEqual(below, -reverse, delta=abs(below) * 1.0e-9)
|
|
self.assertLess(abs(above - below), abs(above) * 0.05)
|
|
self.assertEqual(equal, 0.0)
|
|
self.assertGreater(small, 0.0)
|
|
|
|
def test_center_flow_is_continuous_across_former_relative_deadband(self) -> None:
|
|
base_pressure = 8.0e6
|
|
former_threshold = base_pressure * 1.0e-7
|
|
|
|
below = AmesimPnl0003(
|
|
"below",
|
|
self.medium,
|
|
p1_0=base_pressure + 0.99 * former_threshold,
|
|
p2_0=base_pressure,
|
|
).resistance_mass_flow()
|
|
above = AmesimPnl0003(
|
|
"above",
|
|
self.medium,
|
|
p1_0=base_pressure + 1.01 * former_threshold,
|
|
p2_0=base_pressure,
|
|
).resistance_mass_flow()
|
|
reverse = AmesimPnl0003(
|
|
"reverse",
|
|
self.medium,
|
|
p1_0=base_pressure,
|
|
p2_0=base_pressure + 0.99 * former_threshold,
|
|
).resistance_mass_flow()
|
|
equal = AmesimPnl0003(
|
|
"equal",
|
|
self.medium,
|
|
p1_0=base_pressure,
|
|
p2_0=base_pressure,
|
|
).resistance_mass_flow()
|
|
|
|
self.assertGreater(below, 0.0)
|
|
self.assertGreater(above, below)
|
|
self.assertLess(reverse, 0.0)
|
|
self.assertAlmostEqual(below, -reverse, delta=abs(below) * 1.0e-9)
|
|
self.assertLess(abs(above - below), abs(above) * 0.05)
|
|
self.assertEqual(equal, 0.0)
|
|
|
|
def test_exact_repeated_center_flow_inversion_is_cached(self) -> None:
|
|
pipe = AmesimPnl0003("pnl_3", self.medium)
|
|
pipe._mass_flow_for_pressure_drop.cache_clear()
|
|
|
|
first = pipe._mass_flow_for_pressure_drop(
|
|
1000.0,
|
|
density=1.2,
|
|
temperature=300.0,
|
|
)
|
|
after_first = pipe._mass_flow_for_pressure_drop.cache_info()
|
|
second = pipe._mass_flow_for_pressure_drop(
|
|
1000.0,
|
|
density=1.2,
|
|
temperature=300.0,
|
|
)
|
|
after_second = pipe._mass_flow_for_pressure_drop.cache_info()
|
|
changed_temperature = pipe._mass_flow_for_pressure_drop(
|
|
1000.0,
|
|
density=1.2,
|
|
temperature=400.0,
|
|
)
|
|
after_temperature_change = pipe._mass_flow_for_pressure_drop.cache_info()
|
|
|
|
self.assertEqual(first, second)
|
|
self.assertEqual(after_first.misses, 1)
|
|
self.assertEqual(after_second.hits, after_first.hits + 1)
|
|
self.assertNotEqual(changed_temperature, second)
|
|
self.assertEqual(
|
|
after_temperature_change.misses,
|
|
after_second.misses + 1,
|
|
)
|
|
|
|
def test_pressure_flow_residuals_bind_both_port_pressures_to_states(self) -> None:
|
|
pipe = AmesimPnl0003("pnl_3", self.medium)
|
|
pipe.properties_1()
|
|
pipe.properties_2()
|
|
|
|
residuals = {
|
|
residual.id.rsplit(":", 1)[1]: residual
|
|
for residual in pipe.pressure_flow_equation_residuals()
|
|
}
|
|
|
|
self.assertEqual(set(residuals), {"port_1_pressure_state", "port_2_pressure_state"})
|
|
self.assertAlmostEqual(residuals["port_1_pressure_state"].value, 0.0)
|
|
self.assertAlmostEqual(residuals["port_2_pressure_state"].value, 0.0)
|
|
|
|
def test_connection_derivative_conserves_external_and_center_flows(self) -> None:
|
|
pipe = AmesimPnl0003("pnl_3", self.medium, p1_0=101000.0, p2_0=100000.0)
|
|
port_1 = pipe.properties_1()
|
|
port_2 = pipe.properties_2()
|
|
pipe.port_1.m_flow = 0.2
|
|
pipe.port_2.m_flow = -0.1
|
|
|
|
derivative = pipe.state_derivative_from_ports(
|
|
{"port_1": port_1.h + 1000.0, "port_2": port_2.h - 1000.0}
|
|
)
|
|
|
|
self.assertAlmostEqual(derivative[0] + derivative[2], 0.1)
|
|
|
|
def test_results_include_two_thermodynamic_sides_and_center_flow(self) -> None:
|
|
pipe = AmesimPnl0003("pnl_3", self.medium, p1_0=101000.0, p2_0=100000.0)
|
|
|
|
values = pipe.component_result_values()
|
|
|
|
self.assertIn("m1", values)
|
|
self.assertIn("m2", values)
|
|
self.assertIn("dmctr", values)
|
|
self.assertIn("re", values)
|
|
|
|
def test_result_cm_excludes_center_resistance_flow_coefficient(self) -> None:
|
|
pipe = AmesimPnl0003(
|
|
"pnl_3",
|
|
self.medium,
|
|
diam=0.02,
|
|
le=0.3,
|
|
rr=0.045 / 20.0,
|
|
p1_0=10.7e6,
|
|
T1_0=263.4,
|
|
p2_0=10.68e6,
|
|
T2_0=263.42,
|
|
)
|
|
port_1 = pipe.properties_1()
|
|
port_2 = pipe.properties_2()
|
|
flow = pipe.resistance_mass_flow()
|
|
upstream = port_1 if flow >= 0.0 else port_2
|
|
reynolds = pipe.reynolds_number(flow, upstream.T)
|
|
friction = pipe.friction_factor(reynolds)
|
|
raw_cq_cm = (
|
|
abs(flow)
|
|
* sqrt(upstream.T)
|
|
/ (pipe.area * max(port_1.p, port_2.p))
|
|
)
|
|
flow_coefficient = sqrt(pipe.diam / (pipe.le * friction))
|
|
|
|
actual = pipe.component_result_values()["cm"]
|
|
|
|
self.assertAlmostEqual(actual, raw_cq_cm / flow_coefficient)
|
|
self.assertNotAlmostEqual(actual, raw_cq_cm)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|