242 lines
8.0 KiB
Python
242 lines
8.0 KiB
Python
"""Entropyk — End-to-End Solver Tests.
|
|
|
|
Tests for System construction, finalization, and solving from Python.
|
|
"""
|
|
|
|
import pytest
|
|
import entropyk
|
|
|
|
|
|
class TestSystemConstruction:
|
|
"""Tests for System graph building."""
|
|
|
|
def test_empty_system(self):
|
|
system = entropyk.System()
|
|
assert system.node_count == 0
|
|
assert system.edge_count == 0
|
|
|
|
def test_add_component(self):
|
|
system = entropyk.System()
|
|
idx = system.add_component(entropyk.Condenser(ua=5000.0))
|
|
assert idx == 0
|
|
assert system.node_count == 1
|
|
|
|
def test_add_multiple_components(self):
|
|
system = entropyk.System()
|
|
i0 = system.add_component(entropyk.Compressor())
|
|
i1 = system.add_component(entropyk.Condenser())
|
|
i2 = system.add_component(entropyk.ExpansionValve())
|
|
i3 = system.add_component(entropyk.Evaporator())
|
|
assert system.node_count == 4
|
|
assert i0 != i1 != i2 != i3
|
|
|
|
def test_add_edge(self):
|
|
system = entropyk.System()
|
|
i0 = system.add_component(entropyk.Compressor())
|
|
i1 = system.add_component(entropyk.Condenser())
|
|
edge_idx = system.add_edge(i0, i1)
|
|
assert edge_idx == 0
|
|
assert system.edge_count == 1
|
|
|
|
def test_repr(self):
|
|
system = entropyk.System()
|
|
system.add_component(entropyk.Compressor())
|
|
system.add_component(entropyk.Condenser())
|
|
system.add_edge(0, 1)
|
|
r = repr(system)
|
|
assert "System" in r
|
|
assert "nodes=2" in r
|
|
assert "edges=1" in r
|
|
|
|
|
|
class TestSystemFinalize:
|
|
"""Tests for system finalization."""
|
|
|
|
def test_simple_cycle_finalize(self):
|
|
"""Build and finalize a simple 4-component cycle."""
|
|
system = entropyk.System()
|
|
comp = system.add_component(entropyk.Compressor())
|
|
cond = system.add_component(entropyk.Condenser())
|
|
exv = system.add_component(entropyk.ExpansionValve())
|
|
evap = system.add_component(entropyk.Evaporator())
|
|
|
|
system.add_edge(comp, cond)
|
|
system.add_edge(cond, exv)
|
|
system.add_edge(exv, evap)
|
|
system.add_edge(evap, comp)
|
|
|
|
system.finalize()
|
|
assert system.state_vector_len > 0
|
|
|
|
|
|
class TestSolverConfigs:
|
|
"""Tests for solver configuration objects."""
|
|
|
|
def test_newton_default(self):
|
|
config = entropyk.NewtonConfig()
|
|
assert "NewtonConfig" in repr(config)
|
|
assert "100" in repr(config)
|
|
|
|
def test_newton_custom(self):
|
|
config = entropyk.NewtonConfig(
|
|
max_iterations=200,
|
|
tolerance=1e-8,
|
|
line_search=True,
|
|
timeout_ms=5000,
|
|
)
|
|
assert "200" in repr(config)
|
|
|
|
def test_picard_default(self):
|
|
config = entropyk.PicardConfig()
|
|
assert "PicardConfig" in repr(config)
|
|
|
|
def test_picard_custom(self):
|
|
config = entropyk.PicardConfig(
|
|
max_iterations=300,
|
|
tolerance=1e-5,
|
|
relaxation=0.7,
|
|
)
|
|
assert "300" in repr(config)
|
|
|
|
def test_picard_invalid_relaxation_raises(self):
|
|
with pytest.raises(ValueError, match="between"):
|
|
entropyk.PicardConfig(relaxation=1.5)
|
|
|
|
def test_fallback_default(self):
|
|
config = entropyk.FallbackConfig()
|
|
assert "FallbackConfig" in repr(config)
|
|
|
|
def test_fallback_custom(self):
|
|
newton = entropyk.NewtonConfig(max_iterations=50)
|
|
picard = entropyk.PicardConfig(max_iterations=200)
|
|
config = entropyk.FallbackConfig(newton=newton, picard=picard)
|
|
assert "50" in repr(config)
|
|
|
|
def test_newton_advanced_params(self):
|
|
config = entropyk.NewtonConfig(
|
|
initial_state=[1.0, 2.0, 3.0],
|
|
use_numerical_jacobian=True,
|
|
line_search_armijo_c=1e-3,
|
|
line_search_max_backtracks=10,
|
|
divergence_threshold=1e5
|
|
)
|
|
assert config is not None
|
|
|
|
def test_picard_advanced_params(self):
|
|
config = entropyk.PicardConfig(
|
|
initial_state=[1.0, 2.0],
|
|
timeout_ms=1000,
|
|
)
|
|
assert config is not None
|
|
|
|
def test_convergence_criteria(self):
|
|
cc = entropyk.ConvergenceCriteria(
|
|
pressure_tolerance_pa=2.0,
|
|
mass_balance_tolerance_kgs=1e-8,
|
|
energy_balance_tolerance_w=1e-2
|
|
)
|
|
assert "dP=2.0" in repr(cc)
|
|
assert "dM=1.0" in repr(cc)
|
|
assert "dE=1.0" in repr(cc)
|
|
assert cc.pressure_tolerance_pa == 2.0
|
|
|
|
config = entropyk.NewtonConfig(convergence_criteria=cc)
|
|
assert config is not None
|
|
|
|
def test_jacobian_freezing(self):
|
|
jf = entropyk.JacobianFreezingConfig(max_frozen_iters=5, threshold=0.5)
|
|
assert jf.max_frozen_iters == 5
|
|
assert jf.threshold == 0.5
|
|
config = entropyk.NewtonConfig(jacobian_freezing=jf)
|
|
assert config is not None
|
|
|
|
def test_timeout_config(self):
|
|
tc = entropyk.TimeoutConfig(return_best_state_on_timeout=False, zoh_fallback=True)
|
|
assert not tc.return_best_state_on_timeout
|
|
assert tc.zoh_fallback
|
|
config = entropyk.NewtonConfig(timeout_config=tc)
|
|
assert config is not None
|
|
|
|
def test_solver_strategy(self):
|
|
newton_strat = entropyk.SolverStrategy.newton(tolerance=1e-5)
|
|
picard_strat = entropyk.SolverStrategy.picard(relaxation=0.6)
|
|
default_strat = entropyk.SolverStrategy.default()
|
|
|
|
assert newton_strat is not None
|
|
assert picard_strat is not None
|
|
assert default_strat is not None
|
|
|
|
class TestSolverExecution:
|
|
"""Tests that the bindings actually call the Rust solver engine."""
|
|
|
|
@pytest.fixture
|
|
def simple_system(self):
|
|
system = entropyk.System()
|
|
# Use simple components to avoid complex physics crashes
|
|
i0 = system.add_component(entropyk.Pipe(length=10.0, diameter=0.1, fluid="Water"))
|
|
i1 = system.add_component(entropyk.Pipe(length=10.0, diameter=0.1, fluid="Water"))
|
|
system.add_edge(i0, i1)
|
|
system.add_edge(i1, i0)
|
|
system.finalize()
|
|
return system
|
|
|
|
def test_newton_solve(self, simple_system):
|
|
config = entropyk.NewtonConfig(max_iterations=2, timeout_ms=10)
|
|
try:
|
|
result = config.solve(simple_system)
|
|
assert result is not None
|
|
except entropyk.SolverError:
|
|
# We don't care if it fails to converge, only that it crossed the boundary
|
|
pass
|
|
|
|
def test_picard_solve(self, simple_system):
|
|
config = entropyk.PicardConfig(max_iterations=2, timeout_ms=10)
|
|
try:
|
|
result = config.solve(simple_system)
|
|
assert result is not None
|
|
except entropyk.SolverError:
|
|
pass
|
|
|
|
def test_strategy_solve(self, simple_system):
|
|
strategy = entropyk.SolverStrategy.newton(max_iterations=2, timeout_ms=10)
|
|
try:
|
|
result = strategy.solve(simple_system)
|
|
assert result is not None
|
|
except entropyk.SolverError:
|
|
pass
|
|
|
|
|
|
|
|
class TestConvergedState:
|
|
"""Tests for ConvergedState and ConvergenceStatus types."""
|
|
|
|
def test_convergence_status_repr(self):
|
|
# We can't easily create a ConvergedState without solving,
|
|
# so we just verify the classes exist
|
|
assert hasattr(entropyk, "ConvergedState")
|
|
assert hasattr(entropyk, "ConvergenceStatus")
|
|
|
|
|
|
class TestAllComponentsInSystem:
|
|
"""Test that all component types can be added to a System."""
|
|
|
|
@pytest.mark.parametrize("component_factory", [
|
|
lambda: entropyk.Compressor(),
|
|
lambda: entropyk.Condenser(),
|
|
lambda: entropyk.Evaporator(),
|
|
lambda: entropyk.Economizer(),
|
|
lambda: entropyk.ExpansionValve(),
|
|
lambda: entropyk.Pipe(),
|
|
lambda: entropyk.Pump(),
|
|
lambda: entropyk.Fan(),
|
|
lambda: entropyk.FlowSplitter(),
|
|
lambda: entropyk.FlowMerger(),
|
|
lambda: entropyk.FlowSource(),
|
|
lambda: entropyk.FlowSink(),
|
|
])
|
|
def test_add_component(self, component_factory):
|
|
system = entropyk.System()
|
|
idx = system.add_component(component_factory())
|
|
assert idx >= 0
|
|
assert system.node_count == 1
|