Files
SystemSimulationApp/tests/test_pressure_flow_solver_initialization.py
T

344 lines
13 KiB
Python

from __future__ import annotations
from types import SimpleNamespace
import unittest
from unittest.mock import patch
from app.simulation.components.amesim.boundary.sources import AmesimPnpl01
from app.simulation.components.amesim.flow.orifices import (
AmesimPnor001,
AmesimPnvo001SignalOpening,
)
from app.simulation.components.amesim.flow.pipes import AmesimPnl00r
from app.simulation.components.amesim.flow.pipes import AmesimPnl0001
from app.simulation.components.amesim.flow.pipes import AmesimPnl0002
from app.simulation.components.amesim.junctions.nodes import AmesimPn3Node2
from app.simulation.components.amesim.media.mediums import (
AmesimHeliumPengRobinsonMedium,
)
from app.simulation.components.amesim.storage.chambers import AmesimPnch023
from app.simulation.components.experimental.storage.cylinder import Cylinder
from app.simulation.components.experimental.storage.tank import Tank
from app.simulation.core.medium import IdealGasMedium
from app.simulation.core.state import VolumeState
from app.simulation.solvers.algebraic import AlgebraicSolveError, PressureFlowSolver
from app.simulation.systems.generic import GenericFluidSystem
from app.simulation.systems.network import SimulationNetwork
class PressureFlowSolverInitializationTests(unittest.TestCase):
def test_pnor_pnl0001_series_pressure_is_seeded_by_flow_balance(self) -> None:
medium = IdealGasMedium()
chamber = AmesimPnch023("source", medium, p0=15.3e6)
source_plug = AmesimPnpl01("source_closed")
orifice = AmesimPnor001("orifice", medium)
pipe = AmesimPnl0001("pipe", medium, p0=14.0e6)
storage_plug = AmesimPnpl01("storage_closed")
network = SimulationNetwork("pnor-pnl0001-series")
for component in (chamber, source_plug, orifice, pipe, storage_plug):
network.add_component(component)
network.connect("source_closed", "port_1", "source", "port_1")
network.connect("source", "port_2", "orifice", "port_1")
network.connect("orifice", "port_2", "pipe", "port_1")
network.connect("pipe", "port_2", "storage_closed", "port_1")
chamber.refresh_thermodynamic_ports()
pipe.refresh_thermodynamic_ports()
result = PressureFlowSolver(network).solve()
self.assertTrue(result.success)
self.assertGreater(orifice.port_2.p, pipe.port_2.p)
self.assertLess(orifice.port_2.p, chamber.port_2.p)
self.assertAlmostEqual(orifice.port_2.p, pipe.port_1.p)
self.assertAlmostEqual(-orifice.port_2.m_flow, pipe.port_1.m_flow)
def test_dead_ended_pnl00r_is_seeded_at_zero_flow_pressure(self) -> None:
medium = IdealGasMedium()
pipe = AmesimPnl00r("resistance", medium)
plug = AmesimPnpl01("closed")
pipe.port_1.p = 15.3e6
pipe.port_2.p = 100_000.0
network = SimulationNetwork("dead-ended-resistance")
network.add_component(pipe)
network.add_component(plug)
network.connect("resistance", "port_2", "closed", "port_1")
result = PressureFlowSolver(network).solve()
self.assertTrue(result.success)
self.assertEqual(result.evaluations, 0)
self.assertAlmostEqual(pipe.port_2.p, pipe.port_1.p)
self.assertEqual(pipe.port_1.m_flow, 0.0)
self.assertEqual(pipe.port_2.m_flow, 0.0)
@staticmethod
def _near_equal_pressure_network() -> tuple[
SimulationNetwork,
AmesimHeliumPengRobinsonMedium,
Cylinder,
Tank,
AmesimPnvo001SignalOpening,
]:
medium = AmesimHeliumPengRobinsonMedium()
high = Cylinder("high", medium, V=0.057, p0=1.0e5, T0=256.1)
low = Tank("low", medium, V=0.015, p0=1.0e5, T0=298.2)
valve = AmesimPnvo001SignalOpening(
"valve",
medium,
cq=0.45,
area0=7.85e-5,
gi=1.0,
flowset=1.0,
opening0=1.0,
)
valve.res.signal = 1.0
network = SimulationNetwork("near-equal-pressure")
for component in (high, low, valve):
network.add_component(component)
network.connect("high", "port_b", "valve", "port_2")
network.connect("valve", "port_3", "low", "port_a")
return network, medium, high, low, valve
@staticmethod
def _set_pressure_temperature(
component: Cylinder | Tank,
medium: AmesimHeliumPengRobinsonMedium,
pressure: float,
temperature: float,
) -> None:
mass = medium.density(pressure, temperature) * component.V
component.state = VolumeState(
m=mass,
U=mass
* medium.specific_internal_energy_at_pressure(
pressure,
temperature,
),
)
component.refresh_thermodynamic_ports()
def test_stream_enthalpy_refreshes_explicit_orifice_flow(self) -> None:
network, medium, high, low, valve = self._near_equal_pressure_network()
self._set_pressure_temperature(
high,
medium,
15_201_996.778497815,
292.3997890954483,
)
self._set_pressure_temperature(
low,
medium,
405_072.8123335606,
393.46713105173205,
)
system = GenericFluidSystem(network)
system.consistent_initial_state_vector()
self.assertLess(valve._connected_h["port_2"], 0.0)
self.assertAlmostEqual(
valve.port_2.m_flow,
valve.mass_flow(valve.port_2.p, valve.port_3.p),
places=12,
)
self.assertAlmostEqual(valve.port_3.m_flow, -valve.port_2.m_flow, places=12)
def test_stream_dependent_pipe_flow_closes_with_junction_mixing_in_one_rhs(self) -> None:
medium = IdealGasMedium()
hot = Cylinder("hot", medium, V=0.1, p0=500_000.0, T0=400.0)
cold = Cylinder("cold", medium, V=0.1, p0=500_000.0, T0=250.0)
sink = Tank("sink", medium, V=0.1, p0=100_000.0, T0=300.0)
hot_line = AmesimPnl00r("hot_line", medium, diam=0.01, le=0.5)
cold_line = AmesimPnl00r("cold_line", medium, diam=0.01, le=0.5)
junction = AmesimPn3Node2("junction")
pipe = AmesimPnl0002(
"pipe",
medium,
diam=0.01,
le=1.0,
p0=200_000.0,
T0=300.0,
)
network = SimulationNetwork("stream-flow-fixed-point")
for component in (
hot,
cold,
sink,
hot_line,
cold_line,
junction,
pipe,
):
network.add_component(component)
network.connect("hot", "port_b", "hot_line", "port_1")
network.connect("hot_line", "port_2", "junction", "port_1")
network.connect("cold", "port_b", "cold_line", "port_1")
network.connect("cold_line", "port_2", "junction", "port_3")
network.connect("junction", "port_2", "pipe", "port_1")
network.connect("pipe", "port_2", "sink", "port_a")
system = GenericFluidSystem(network)
state = system.consistent_initial_state_vector()
first = system.rhs(0.0, state)
second = system.rhs(0.0, state)
# PN3NODE2 takes its pressure-flow temperature reference from port 2,
# so that closure no longer depends on the flow-weighted energy mix.
self.assertGreaterEqual(system.max_thermofluid_iterations, 1)
for first_value, second_value in zip(first, second):
self.assertAlmostEqual(first_value, second_value, delta=1.0e-8)
def test_current_storage_pressure_reseeds_stale_orifice_ports_and_flow(self) -> None:
network, medium, high, low, valve = self._near_equal_pressure_network()
solver = PressureFlowSolver(network, max_evaluations=10)
high_pressure = 10_790_000.0
self._set_pressure_temperature(high, medium, high_pressure, 256.1)
self._set_pressure_temperature(low, medium, high_pressure - 300.0, 298.2)
initial = solver.solve()
self.assertTrue(initial.success)
stale_low_pressure = valve.port_3.p
stale_flow = valve.port_2.m_flow
self._set_pressure_temperature(low, medium, high_pressure - 100.0, 298.2)
self.assertNotAlmostEqual(low.port_a.p, stale_low_pressure, places=3)
updated = solver.solve()
self.assertTrue(updated.success)
self.assertEqual(updated.evaluations, 0)
self.assertAlmostEqual(valve.port_2.p, high.port_b.p, places=6)
self.assertAlmostEqual(valve.port_3.p, low.port_a.p, places=6)
self.assertNotAlmostEqual(valve.port_2.m_flow, stale_flow, places=8)
self.assertAlmostEqual(
valve.port_2.m_flow,
valve.mass_flow(valve.port_2.p, valve.port_3.p),
places=10,
)
self.assertAlmostEqual(high.port_b.m_flow, -valve.port_2.m_flow, places=10)
self.assertAlmostEqual(low.port_a.m_flow, -valve.port_3.m_flow, places=10)
def test_compiled_plan_does_not_rebuild_equation_metadata_during_solve(self) -> None:
boundary = AmesimPnpl01("compiled_closed")
boundary.port_1.p = 100_000.0
boundary.port_1.m_flow = 1.0
network = SimulationNetwork("compiled-closed-boundary")
network.add_component(boundary)
solver = PressureFlowSolver(network)
with patch.object(
boundary,
"pressure_flow_equation_residuals",
side_effect=AssertionError("equation metadata rebuilt at runtime"),
):
diagnostics = solver.solve()
self.assertTrue(diagnostics.success)
self.assertEqual(diagnostics.evaluations, 0)
self.assertEqual(boundary.port_1.m_flow, 0.0)
@staticmethod
def _closed_boundary_solver() -> PressureFlowSolver:
boundary = AmesimPnpl01("closed")
boundary.port_1.p = 100_000.0
boundary.port_1.m_flow = 1.0
network = SimulationNetwork("closed-boundary")
network.add_component(boundary)
return PressureFlowSolver(network)
@staticmethod
def _least_squares_result(x, *, status: int):
return SimpleNamespace(
x=x,
success=status > 0,
status=status,
message="test optimizer result",
nfev=1,
)
def test_status_zero_is_accepted_only_for_finite_converged_residuals(self) -> None:
exact_solver = self._closed_boundary_solver()
def exact_status_zero(_fun, x0, **_kwargs):
values = x0.copy()
flow_index = next(
index
for index, unknown in enumerate(exact_solver.unknowns)
if unknown.variable == "m_flow"
)
values[flow_index] = 0.0
return self._least_squares_result(values, status=0)
with patch.object(
PressureFlowSolver,
"_solve_explicit_flow_unknowns",
return_value=None,
), patch("scipy.optimize.least_squares", side_effect=exact_status_zero):
diagnostics = exact_solver.solve()
self.assertTrue(diagnostics.success)
self.assertEqual(diagnostics.max_scaled_residual, 0.0)
inaccurate_solver = self._closed_boundary_solver()
def inaccurate_status_zero(_fun, x0, **_kwargs):
values = x0.copy()
flow_index = next(
index
for index, unknown in enumerate(inaccurate_solver.unknowns)
if unknown.variable == "m_flow"
)
values[flow_index] = 1.0
return self._least_squares_result(values, status=0)
with patch.object(
PressureFlowSolver,
"_solve_explicit_flow_unknowns",
return_value=None,
), patch("scipy.optimize.least_squares", side_effect=inaccurate_status_zero):
with self.assertRaises(AlgebraicSolveError):
inaccurate_solver.solve()
invalid_status_solver = self._closed_boundary_solver()
def exact_invalid_status(_fun, x0, **_kwargs):
values = x0.copy()
flow_index = next(
index
for index, unknown in enumerate(invalid_status_solver.unknowns)
if unknown.variable == "m_flow"
)
values[flow_index] = 0.0
return self._least_squares_result(values, status=-1)
with patch.object(
PressureFlowSolver,
"_solve_explicit_flow_unknowns",
return_value=None,
), patch("scipy.optimize.least_squares", side_effect=exact_invalid_status):
with self.assertRaises(AlgebraicSolveError):
invalid_status_solver.solve()
def test_zero_residual_seed_does_not_bypass_positive_pressure_bound(self) -> None:
medium = IdealGasMedium()
tank = Tank("tank", medium, V=1.0)
plug = AmesimPnpl01("plug")
network = SimulationNetwork("invalid-negative-pressure")
network.add_component(tank)
network.add_component(plug)
network.connect("tank", "port_a", "plug", "port_1")
tank.state = VolumeState(m=1.0, U=-1000.0)
tank.refresh_thermodynamic_ports()
self.assertLess(tank.port_a.p, 0.0)
solver = PressureFlowSolver(network, max_evaluations=10)
with self.assertRaises(AlgebraicSolveError):
solver.solve()
self.assertIsNotNone(solver.last_diagnostics)
self.assertFalse(solver.last_diagnostics.success)
if __name__ == "__main__":
unittest.main()