"""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