278 lines
11 KiB
Python
278 lines
11 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.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_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)
|
|
|
|
@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()
|