from __future__ import annotations import unittest from math import log10, pi, sqrt from app.simulation.components.amesim.flow.pipes import AmesimPnl0002, AmesimPnl0003 from app.simulation.components.amesim.media.mediums import ( AmesimHeliumPengRobinsonMedium, ) 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_reported_reynolds_uses_amesim_helium_nasa_viscosity_only(self) -> None: medium = AmesimHeliumPengRobinsonMedium() pipe = AmesimPnl0003("pnl_3", medium, diam=0.02) mass_flow = 0.04555349965141288 upstream_temperature = 233.92077861710192 # Frozen AMESim dense row: t=0.81 s, branch 2, D=0.02 m. expected_amesim_reynolds = 172298.96343252173 reported = pipe.reported_reynolds_number( mass_flow, upstream_temperature, ) dynamic = pipe.reynolds_number(mass_flow, upstream_temperature) self.assertAlmostEqual(reported, expected_amesim_reynolds, delta=1.0e-9) self.assertNotAlmostEqual(reported, dynamic, delta=1.0) self.assertAlmostEqual( dynamic, 4.0 * abs(mass_flow) / ( pi * pipe.diam * medium.dynamic_viscosity(upstream_temperature) ), ) 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()