diff --git a/app/simulation/components/amesim/flow/orifices.py b/app/simulation/components/amesim/flow/orifices.py index b537427..01001fd 100644 --- a/app/simulation/components/amesim/flow/orifices.py +++ b/app/simulation/components/amesim/flow/orifices.py @@ -716,6 +716,7 @@ class AmesimPnvo001FixedOpening(AlgebraicComponent): upstream_temperature=self._upstream_temperature("port_3"), ) + @lru_cache(maxsize=32768) def _one_way_flow_characteristics( self, *, diff --git a/app/simulation/components/amesim/flow/pipes.py b/app/simulation/components/amesim/flow/pipes.py index ceefc07..ad8911a 100644 --- a/app/simulation/components/amesim/flow/pipes.py +++ b/app/simulation/components/amesim/flow/pipes.py @@ -267,6 +267,7 @@ class AmesimPnl00r(AlgebraicComponent): ) return magnitude if mass_flow > 0.0 else -magnitude + @lru_cache(maxsize=32768) def _mass_flow_for_pressure_drop( self, pressure_drop: float, @@ -1378,6 +1379,7 @@ class AmesimPnl0003(DynamicComponent): magnitude = friction * (self.le / self.diam) * density * velocity * velocity / 2.0 return magnitude if mass_flow > 0.0 else -magnitude + @lru_cache(maxsize=32768) def _mass_flow_for_pressure_drop( self, pressure_drop: float, diff --git a/app/simulation/solvers/algebraic.py b/app/simulation/solvers/algebraic.py index 95f83d2..40c2840 100644 --- a/app/simulation/solvers/algebraic.py +++ b/app/simulation/solvers/algebraic.py @@ -327,6 +327,14 @@ class PressureFlowSolver: selected: self._filter_explicit_flow_plan(selected) for selected in (frozenset(("f", "m_flow")), frozenset(("f",))) } + self._explicit_flow_unknowns_by_variables = { + selected: tuple( + unknown + for unknown in self.unknowns + if unknown.variable in selected + ) + for selected in self._explicit_flow_plans_by_variables + } self._equation_scale_plans = self._build_equation_scale_plans( self._equation_templates ) @@ -1519,6 +1527,25 @@ class PressureFlowSolver: return read_component_equation + def _connection_flow_target_reader( + self, + equation, + unknown: AlgebraicUnknown, + ) -> Callable[[], float]: + if equation.relation != "sumToZero" or len(equation.variables) != 2: + raise ValueError( + f"Connection flow equation {equation.id} must sum two variables." + ) + first = self._unknowns_by_id[equation.variables[0]] + second = self._unknowns_by_id[equation.variables[1]] + if unknown is first: + return lambda: 0.0 - (0.0 + second.read()) + if unknown is second: + return lambda: 0.0 - (first.read() + 0.0) + raise ValueError( + f"Connection flow equation {equation.id} does not contain {unknown.id}." + ) + def _pressure_flow_equation_values(self) -> tuple[float, ...]: """Evaluate live equation values through the compiled topology.""" @@ -1562,7 +1589,7 @@ class PressureFlowSolver: return ExplicitFlowAssignment( equation_id=equation.id, unknown=unknown, - evaluate=self._equation_value_reader(equation), + evaluate=self._connection_flow_target_reader(equation, unknown), ) evaluation = self._component_equation_plans_by_id[equation.owner_id] @@ -1747,6 +1774,7 @@ class PressureFlowSolver: def _evaluate_explicit_flow_stage( stage: ExplicitFlowStage, ) -> tuple[float, ...]: + """Evaluate simultaneous targets while this stage's unknowns are zero.""" assignments = stage.assignments values: list[float | None] = [None] * len(assignments) for assignment_index, evaluate in stage.direct_evaluations: @@ -1764,7 +1792,9 @@ class PressureFlowSolver: "Compiled algebraic equation disappeared at runtime: " f"{equation_id}." ) - values[assignment_index] = float(equation_values[equation_index]) + values[assignment_index] = 0.0 - float( + equation_values[equation_index] + ) if any(value is None for value in values): raise RuntimeError("Explicit flow evaluation plan returned no value.") return tuple(float(value) for value in values) @@ -1776,9 +1806,15 @@ class PressureFlowSolver: """Execute staged flow/force assignments without repeated equations.""" selected = frozenset(variables) - for unknown in self.unknowns: - if unknown.variable in selected: - unknown.write(0.0) + reset_unknowns = self._explicit_flow_unknowns_by_variables.get(selected) + if reset_unknowns is None: + reset_unknowns = tuple( + unknown + for unknown in self.unknowns + if unknown.variable in selected + ) + for unknown in reset_unknowns: + unknown.write(0.0) seeded_ids: set[str] = set() plan = self._explicit_flow_plans_by_variables.get(selected) @@ -1787,14 +1823,7 @@ class PressureFlowSolver: for stage in plan: assignments = stage.assignments values = self._evaluate_explicit_flow_stage(stage) - targets = tuple( - ( - assignment, - assignment.unknown.read() - value, - ) - for assignment, value in zip(assignments, values) - ) - for assignment, target_value in targets: + for assignment, target_value in zip(assignments, values): if not isfinite(target_value): continue assignment.unknown.write(target_value) diff --git a/app/simulation/solvers/algebraic_blocks.py b/app/simulation/solvers/algebraic_blocks.py index 83325ee..cadbe3a 100644 --- a/app/simulation/solvers/algebraic_blocks.py +++ b/app/simulation/solvers/algebraic_blocks.py @@ -779,14 +779,7 @@ class StreamPressureBlockSolver: unknown.write(0.0) for stage in self._selected_explicit_flow_plan: values = solver._evaluate_explicit_flow_stage(stage) - targets = tuple( - ( - assignment, - assignment.unknown.read() - value, - ) - for assignment, value in zip(stage.assignments, values) - ) - for assignment, target_value in targets: + for assignment, target_value in zip(stage.assignments, values): if isfinite(target_value): assignment.unknown.write(target_value) seeded_equation_ids.add(assignment.equation_id) diff --git a/tests/test_amesim_pnl0002_pnl0003_component.py b/tests/test_amesim_pnl0002_pnl0003_component.py index 2af1c5f..0aefc27 100644 --- a/tests/test_amesim_pnl0002_pnl0003_component.py +++ b/tests/test_amesim_pnl0002_pnl0003_component.py @@ -320,6 +320,38 @@ class AmesimPnl0003ComponentTests(unittest.TestCase): 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() diff --git a/tests/test_amesim_pnl00r_component.py b/tests/test_amesim_pnl00r_component.py index b530207..b53c416 100644 --- a/tests/test_amesim_pnl00r_component.py +++ b/tests/test_amesim_pnl00r_component.py @@ -77,6 +77,34 @@ class AmesimPnl00rComponentTests(unittest.TestCase): self.assertGreater(pipe.mass_flow(15.3e6 + 0.1, 15.3e6), 0.0) + def test_pressure_drop_inversion_cache_keys_all_property_inputs(self) -> None: + pipe = AmesimPnl00r( + "pnl_1", + self.medium, + diam=0.014, + le=1.0, + rr=0.045 / 14.0, + ) + pipe.port_1.h_outflow = self.medium.specific_enthalpy(300.0) + pipe._mass_flow_for_pressure_drop.cache_clear() + + first = pipe.mass_flow(501000.0, 500000.0) + after_first = pipe._mass_flow_for_pressure_drop.cache_info() + second = pipe.mass_flow(501000.0, 500000.0) + after_second = pipe._mass_flow_for_pressure_drop.cache_info() + pipe.port_1.h_outflow = self.medium.specific_enthalpy(400.0) + third = pipe.mass_flow(501000.0, 500000.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(third, second) + self.assertEqual( + after_temperature_change.misses, + after_second.misses + 1, + ) + def test_friction_factor_transitions_from_laminar_to_turbulent(self) -> None: pipe = AmesimPnl00r("pnl_1", self.medium, rr=1e-5) diff --git a/tests/test_amesim_signal_components.py b/tests/test_amesim_signal_components.py index 40690c3..340f56c 100644 --- a/tests/test_amesim_signal_components.py +++ b/tests/test_amesim_signal_components.py @@ -100,6 +100,38 @@ class AmesimSignalComponentTests(unittest.TestCase): valve.res.signal = -0.5 self.assertAlmostEqual(valve.opening, 0.0) + def test_pnvo001_exact_repeated_flow_characteristics_are_cached(self) -> None: + valve = AmesimPnvo001SignalOpening("valve_1", self.medium, opening0=0.25) + valve._one_way_flow_characteristics.cache_clear() + + first = valve._one_way_flow_characteristics( + upstream_pressure=501000.0, + downstream_pressure=500000.0, + upstream_temperature=300.0, + ) + after_first = valve._one_way_flow_characteristics.cache_info() + second = valve._one_way_flow_characteristics( + upstream_pressure=501000.0, + downstream_pressure=500000.0, + upstream_temperature=300.0, + ) + after_second = valve._one_way_flow_characteristics.cache_info() + changed_pressure = valve._one_way_flow_characteristics( + upstream_pressure=502000.0, + downstream_pressure=500000.0, + upstream_temperature=300.0, + ) + after_pressure_change = valve._one_way_flow_characteristics.cache_info() + + self.assertEqual(first, second) + self.assertEqual(after_first.misses, 1) + self.assertEqual(after_second.hits, after_first.hits + 1) + self.assertNotEqual(changed_pressure, second) + self.assertEqual( + after_pressure_change.misses, + after_second.misses + 1, + ) + def test_signal_resolver_propagates_step_to_valve_input(self) -> None: network = SimulationNetwork("signal-smoke") step = AmesimStep0("step_1", self.medium, initial=0.0, final=0.75, time=0.1)