Replace Python numerical kernels with native C execution
This commit is contained in:
1 parent
48da6be21c
commit
3b38f73fe0
227 files changed
+16801
-75499
No files matched your search
@@ -13,60 +13,14 @@ from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
|
||||
|
||||
|
||||
class _ScaledReynoldsPnl0001(AmesimPnl0001):
|
||||
def __init__(self, *args, reynolds_scale: float, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.reynolds_scale = float(reynolds_scale)
|
||||
self.reynolds_calls: list[tuple[float, float]] = []
|
||||
|
||||
def reynolds_number(self, mass_flow: float, temperature: float) -> float:
|
||||
self.reynolds_calls.append((mass_flow, temperature))
|
||||
return self.reynolds_scale * super().reynolds_number(
|
||||
mass_flow,
|
||||
temperature,
|
||||
)
|
||||
|
||||
|
||||
class _DelegatingFrictionPnl0001(AmesimPnl0001):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.friction_reynolds: list[float] = []
|
||||
|
||||
def friction_factor(self, reynolds_number: float) -> float:
|
||||
self.friction_reynolds.append(reynolds_number)
|
||||
return super().friction_factor(reynolds_number)
|
||||
|
||||
|
||||
class AmesimPnl0001ComponentTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.medium = IdealGasMedium()
|
||||
|
||||
def test_default_create_preserves_amesim_parameter_contract(self) -> None:
|
||||
pipe = COMPONENT_MODEL_REGISTRY["amesim_pnl0001"].create(
|
||||
"pnl_1",
|
||||
self.medium,
|
||||
{},
|
||||
)
|
||||
|
||||
self.assertIsInstance(pipe, AmesimPnl0001)
|
||||
self.assertEqual(set(pipe.ports), {"port_1", "port_2"})
|
||||
self.assertEqual(
|
||||
set(pipe.parameter_values),
|
||||
{
|
||||
"diam",
|
||||
"le",
|
||||
"rr",
|
||||
"k",
|
||||
"kth",
|
||||
"extemp",
|
||||
"gi",
|
||||
"mode",
|
||||
"p0",
|
||||
"T0",
|
||||
},
|
||||
)
|
||||
self.assertEqual(len(pipe.get_state_vector()), 2)
|
||||
self.assertAlmostEqual(pipe.volume, 7.853981633974483e-5)
|
||||
|
||||
def test_rejects_fractional_integer_parameters(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "gi must be an integer"):
|
||||
@@ -82,298 +36,19 @@ class AmesimPnl0001ComponentTests(unittest.TestCase):
|
||||
{"mode": 1.5},
|
||||
)
|
||||
|
||||
def test_initial_state_uses_pipe_volume_pressure_and_temperature(self) -> None:
|
||||
pipe = AmesimPnl0001(
|
||||
"pnl_1",
|
||||
self.medium,
|
||||
diam=0.014,
|
||||
le=1.0,
|
||||
rr=0.045 / 14.0,
|
||||
p0=15.3e6,
|
||||
T0=293.15,
|
||||
)
|
||||
props = pipe.properties()
|
||||
|
||||
self.assertAlmostEqual(pipe.volume, 1.539380400258999e-4)
|
||||
self.assertAlmostEqual(props.p, 15.3e6, delta=1.0e-5)
|
||||
self.assertAlmostEqual(props.T, 293.15)
|
||||
self.assertAlmostEqual(pipe.port_2.p, props.p)
|
||||
|
||||
def test_resistance_flow_follows_pressure_gradient(self) -> None:
|
||||
pipe = AmesimPnl0001(
|
||||
"pnl_1",
|
||||
self.medium,
|
||||
diam=0.014,
|
||||
le=1.0,
|
||||
rr=0.045 / 14.0,
|
||||
p0=15.3e6,
|
||||
T0=293.15,
|
||||
)
|
||||
props = pipe.properties()
|
||||
|
||||
forward = pipe.mass_flow(15.31e6, props.p, props.T)
|
||||
reverse = pipe.mass_flow(15.29e6, props.p, props.T)
|
||||
|
||||
self.assertGreater(forward, 0.0)
|
||||
self.assertLess(reverse, 0.0)
|
||||
self.assertAlmostEqual(abs(forward), abs(reverse), delta=abs(forward) * 0.02)
|
||||
|
||||
def test_high_pressure_near_equal_flow_is_continuous_outside_roundoff(self) -> None:
|
||||
pipe = AmesimPnl0001("pnl_1", self.medium, p0=15.3e6)
|
||||
|
||||
small = pipe.mass_flow(15.3e6 + 0.1, 15.3e6, 293.15)
|
||||
larger = pipe.mass_flow(15.3e6 + 1.0, 15.3e6, 293.15)
|
||||
|
||||
self.assertGreater(small, 0.0)
|
||||
self.assertGreater(larger, small)
|
||||
|
||||
def test_pressure_roundoff_does_not_create_a_fictitious_flow(self) -> None:
|
||||
pipe = AmesimPnl0001("pnl_1", self.medium, p0=15.3e6)
|
||||
|
||||
self.assertEqual(pipe.mass_flow(15.3e6, 15.3e6 + 2.0e-9, 293.15), 0.0)
|
||||
|
||||
def test_overridden_reynolds_number_is_called_on_every_iteration(self) -> None:
|
||||
slow_reynolds = _ScaledReynoldsPnl0001(
|
||||
"slow_reynolds",
|
||||
self.medium,
|
||||
reynolds_scale=0.25,
|
||||
)
|
||||
fast_reynolds = _ScaledReynoldsPnl0001(
|
||||
"fast_reynolds",
|
||||
self.medium,
|
||||
reynolds_scale=4.0,
|
||||
)
|
||||
options = {
|
||||
"upstream_pressure": 2.0e6,
|
||||
"downstream_pressure": 1.0e6,
|
||||
"upstream_temperature": 300.0,
|
||||
"resistance_length": 1.0,
|
||||
"max_iterations": 4,
|
||||
}
|
||||
|
||||
slow_flow = slow_reynolds._one_way_pn2pipefr_mass_flow(**options)
|
||||
fast_flow = fast_reynolds._one_way_pn2pipefr_mass_flow(**options)
|
||||
|
||||
self.assertEqual(len(slow_reynolds.reynolds_calls), 4)
|
||||
self.assertEqual(len(fast_reynolds.reynolds_calls), 4)
|
||||
self.assertTrue(
|
||||
all(
|
||||
temperature == 300.0
|
||||
for _mass_flow, temperature in slow_reynolds.reynolds_calls
|
||||
)
|
||||
)
|
||||
self.assertLess(slow_flow, fast_flow)
|
||||
|
||||
def test_zero_iterations_does_not_evaluate_dynamic_viscosity(self) -> None:
|
||||
pipe = AmesimPnl0001("pnl_1", self.medium)
|
||||
original = IdealGasMedium.dynamic_viscosity
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
IdealGasMedium,
|
||||
"dynamic_viscosity",
|
||||
autospec=True,
|
||||
side_effect=original,
|
||||
) as dynamic_viscosity,
|
||||
patch(
|
||||
"app.simulation.components.amesim.flow.pipes.log10",
|
||||
side_effect=log10,
|
||||
) as logarithm,
|
||||
):
|
||||
flow = pipe._one_way_pn2pipefr_mass_flow(
|
||||
upstream_pressure=2.0e6,
|
||||
downstream_pressure=1.0e6,
|
||||
upstream_temperature=300.0,
|
||||
resistance_length=1.0,
|
||||
max_iterations=0,
|
||||
)
|
||||
|
||||
self.assertGreater(flow, 0.0)
|
||||
dynamic_viscosity.assert_not_called()
|
||||
logarithm.assert_not_called()
|
||||
|
||||
def test_builtin_iterations_evaluate_dynamic_viscosity_once(self) -> None:
|
||||
pipe = AmesimPnl0001("pnl_1", self.medium)
|
||||
original = IdealGasMedium.dynamic_viscosity
|
||||
|
||||
with patch.object(
|
||||
IdealGasMedium,
|
||||
"dynamic_viscosity",
|
||||
autospec=True,
|
||||
side_effect=original,
|
||||
) as dynamic_viscosity:
|
||||
flow = pipe._one_way_pn2pipefr_mass_flow(
|
||||
upstream_pressure=2.0e6,
|
||||
downstream_pressure=1.0e6,
|
||||
upstream_temperature=300.0,
|
||||
resistance_length=1.0,
|
||||
max_iterations=4,
|
||||
)
|
||||
|
||||
self.assertGreater(flow, 0.0)
|
||||
dynamic_viscosity.assert_called_once_with(self.medium, 300.0)
|
||||
|
||||
def test_builtin_fixed_point_evaluates_fully_rough_term_once(self) -> None:
|
||||
options = {
|
||||
"upstream_pressure": 2.0e6,
|
||||
"downstream_pressure": 1.0e6,
|
||||
"upstream_temperature": 300.0,
|
||||
"resistance_length": 1.0,
|
||||
"max_iterations": 4,
|
||||
}
|
||||
pipes = (
|
||||
AmesimPnl00r("pnl_00r", self.medium, rr=1.0e-5),
|
||||
AmesimPnl0001("pnl_0001", self.medium, rr=1.0e-5),
|
||||
AmesimPnl0002("pnl_0002", self.medium, rr=1.0e-5),
|
||||
)
|
||||
|
||||
for pipe in pipes:
|
||||
with self.subTest(model=type(pipe).__name__):
|
||||
with patch(
|
||||
"app.simulation.components.amesim.flow.pipes.log10",
|
||||
side_effect=log10,
|
||||
) as logarithm:
|
||||
flow = AmesimPnl0001._one_way_pn2pipefr_mass_flow(
|
||||
pipe,
|
||||
**options,
|
||||
)
|
||||
|
||||
self.assertGreater(flow, 0.0)
|
||||
fully_rough_argument = pipe.rr / 3.7
|
||||
self.assertEqual(
|
||||
sum(
|
||||
call.args == (fully_rough_argument,)
|
||||
for call in logarithm.call_args_list
|
||||
),
|
||||
1,
|
||||
)
|
||||
self.assertEqual(logarithm.call_count, 5)
|
||||
|
||||
def test_overridden_friction_factor_keeps_virtual_iteration_path(self) -> None:
|
||||
pipe = _DelegatingFrictionPnl0001(
|
||||
"custom_friction",
|
||||
self.medium,
|
||||
rr=1.0e-5,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"app.simulation.components.amesim.flow.pipes.log10",
|
||||
side_effect=log10,
|
||||
) as logarithm:
|
||||
flow = pipe._one_way_pn2pipefr_mass_flow(
|
||||
upstream_pressure=2.0e6,
|
||||
downstream_pressure=1.0e6,
|
||||
upstream_temperature=300.0,
|
||||
resistance_length=1.0,
|
||||
max_iterations=4,
|
||||
)
|
||||
|
||||
self.assertGreater(flow, 0.0)
|
||||
self.assertEqual(len(pipe.friction_reynolds), 4)
|
||||
self.assertEqual(logarithm.call_count, 8)
|
||||
|
||||
def test_precomputed_fully_rough_path_is_bitwise_identical_by_regime(
|
||||
self,
|
||||
) -> None:
|
||||
for index, downstream_pressure in enumerate(
|
||||
(1_999_999.999, 1_999_990.0, 1_999_900.0, 1.0e6)
|
||||
):
|
||||
with self.subTest(downstream_pressure=downstream_pressure):
|
||||
built_in = AmesimPnl0001(
|
||||
f"built_in_{index}",
|
||||
self.medium,
|
||||
rr=1.0e-5,
|
||||
)
|
||||
virtual = _DelegatingFrictionPnl0001(
|
||||
f"virtual_{index}",
|
||||
self.medium,
|
||||
rr=1.0e-5,
|
||||
)
|
||||
options = {
|
||||
"upstream_pressure": 2.0e6,
|
||||
"downstream_pressure": downstream_pressure,
|
||||
"upstream_temperature": 300.0,
|
||||
"resistance_length": 1.0,
|
||||
"max_iterations": 4,
|
||||
}
|
||||
|
||||
actual = built_in._one_way_pn2pipefr_mass_flow(**options)
|
||||
expected = virtual._one_way_pn2pipefr_mass_flow(**options)
|
||||
|
||||
self.assertEqual(actual.hex(), expected.hex())
|
||||
|
||||
def test_pressure_flow_residuals_do_not_force_storage_mass_balance(self) -> None:
|
||||
pipe = AmesimPnl0001("pnl_1", self.medium)
|
||||
pipe.port_1.p = 101000.0
|
||||
pipe.port_2.m_flow = -1.0e-4
|
||||
props = pipe.properties()
|
||||
pipe.port_1.m_flow = pipe.mass_flow(pipe.port_1.p, pipe.port_2.p, props.T)
|
||||
|
||||
residuals = {
|
||||
residual.id.rsplit(":", 1)[1]: residual
|
||||
for residual in pipe.pressure_flow_equation_residuals()
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
set(residuals),
|
||||
{"port_2_pressure_state", "port_1_pressure_flow_relation"},
|
||||
)
|
||||
self.assertAlmostEqual(residuals["port_2_pressure_state"].value, 0.0)
|
||||
self.assertAlmostEqual(residuals["port_1_pressure_flow_relation"].value, 0.0)
|
||||
|
||||
def test_connection_derivative_preserves_two_port_accumulation(self) -> None:
|
||||
pipe = AmesimPnl0001("pnl_1", self.medium, kth=2.0, extemp=310.0)
|
||||
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)
|
||||
self.assertGreater(derivative[1], 0.0)
|
||||
|
||||
def test_results_include_thermodynamic_and_pipe_diagnostics(self) -> None:
|
||||
pipe = AmesimPnl0001("pnl_1", self.medium)
|
||||
pipe.port_1.p = 101000.0
|
||||
|
||||
values = pipe.component_result_values()
|
||||
|
||||
self.assertEqual(
|
||||
set(values),
|
||||
{"m", "U", "p", "T", "rho", "u", "h", "re", "cm", "v", "ff"},
|
||||
)
|
||||
|
||||
def test_result_cm_excludes_pipe_flow_coefficient(self) -> None:
|
||||
pipe = AmesimPnl0001(
|
||||
"pnl_1",
|
||||
self.medium,
|
||||
diam=0.014,
|
||||
le=0.3,
|
||||
rr=0.045 / 14.0,
|
||||
p0=2.5e6,
|
||||
T0=290.0,
|
||||
)
|
||||
pipe.port_1.p = 1.6e6
|
||||
props = pipe.properties()
|
||||
flow = pipe.mass_flow(pipe.port_1.p, props.p, props.T)
|
||||
reynolds = pipe.reynolds_number(flow, props.T)
|
||||
friction = pipe.friction_factor(reynolds)
|
||||
raw_cq_cm = (
|
||||
abs(flow)
|
||||
* sqrt(props.T)
|
||||
/ (pipe.area * max(pipe.port_1.p, props.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__":
|
||||
|
||||
Reference in new issue
Block a user