199 lines
7.3 KiB
Python
199 lines
7.3 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 (
|
|
AmesimPnvo001SignalOpening,
|
|
)
|
|
from app.simulation.components.amesim.media.mediums import (
|
|
AmesimHeliumPengRobinsonMedium,
|
|
)
|
|
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.network import SimulationNetwork
|
|
|
|
|
|
class PressureFlowSolverInitializationTests(unittest.TestCase):
|
|
@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(temperature),
|
|
)
|
|
component.refresh_thermodynamic_ports()
|
|
|
|
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,
|
|
"_seed_explicit_mass_flows",
|
|
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,
|
|
"_seed_explicit_mass_flows",
|
|
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,
|
|
"_seed_explicit_mass_flows",
|
|
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()
|