feat(python): implement python bindings for all components and solvers
This commit is contained in:
0
bindings/python/tests/__init__.py
Normal file
0
bindings/python/tests/__init__.py
Normal file
BIN
bindings/python/tests/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
bindings/python/tests/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
bindings/python/tests/conftest.py
Normal file
1
bindings/python/tests/conftest.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Pytest configuration for Entropyk Python bindings tests."""
|
||||
98
bindings/python/tests/test_benchmark.py
Normal file
98
bindings/python/tests/test_benchmark.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Entropyk — Performance Benchmark Tests.
|
||||
|
||||
Tests that measure Python→Rust call overhead and verify performance.
|
||||
These are not unit tests — they measure timing and should be run with
|
||||
``pytest -s`` for visible output.
|
||||
"""
|
||||
|
||||
import time
|
||||
import pytest
|
||||
import entropyk
|
||||
|
||||
|
||||
class TestConstructorOverhead:
|
||||
"""Benchmark component construction overhead."""
|
||||
|
||||
def test_1000_compressor_constructions(self):
|
||||
"""Constructing 1000 Compressors should be very fast (< 100 ms)."""
|
||||
start = time.perf_counter()
|
||||
for _ in range(1000):
|
||||
entropyk.Compressor()
|
||||
elapsed = time.perf_counter() - start
|
||||
assert elapsed < 0.1, f"1000 Compressor constructions took {elapsed:.3f}s"
|
||||
|
||||
def test_1000_pressure_constructions(self):
|
||||
"""Constructing 1000 Pressure objects should be very fast."""
|
||||
start = time.perf_counter()
|
||||
for _ in range(1000):
|
||||
entropyk.Pressure(bar=1.0)
|
||||
elapsed = time.perf_counter() - start
|
||||
assert elapsed < 0.1, f"1000 Pressure constructions took {elapsed:.3f}s"
|
||||
|
||||
def test_1000_temperature_constructions(self):
|
||||
"""Constructing 1000 Temperature objects should be very fast."""
|
||||
start = time.perf_counter()
|
||||
for _ in range(1000):
|
||||
entropyk.Temperature(celsius=25.0)
|
||||
elapsed = time.perf_counter() - start
|
||||
assert elapsed < 0.1, f"1000 Temperature constructions took {elapsed:.3f}s"
|
||||
|
||||
|
||||
class TestConversionOverhead:
|
||||
"""Benchmark unit conversion overhead."""
|
||||
|
||||
def test_1000_pressure_conversions(self):
|
||||
"""Unit conversions should add negligible overhead."""
|
||||
p = entropyk.Pressure(bar=1.0)
|
||||
start = time.perf_counter()
|
||||
for _ in range(1000):
|
||||
_ = p.to_bar()
|
||||
_ = p.to_pascals()
|
||||
_ = p.to_kpa()
|
||||
elapsed = time.perf_counter() - start
|
||||
assert elapsed < 0.1, f"3000 pressure conversions took {elapsed:.3f}s"
|
||||
|
||||
def test_1000_temperature_conversions(self):
|
||||
"""Temperature conversions should be fast."""
|
||||
t = entropyk.Temperature(celsius=25.0)
|
||||
start = time.perf_counter()
|
||||
for _ in range(1000):
|
||||
_ = t.to_celsius()
|
||||
_ = t.to_kelvin()
|
||||
_ = t.to_fahrenheit()
|
||||
elapsed = time.perf_counter() - start
|
||||
assert elapsed < 0.1, f"3000 temperature conversions took {elapsed:.3f}s"
|
||||
|
||||
|
||||
class TestArithmeticOverhead:
|
||||
"""Benchmark arithmetic operation overhead."""
|
||||
|
||||
def test_1000_additions(self):
|
||||
"""1000 pressure additions should be fast."""
|
||||
p1 = entropyk.Pressure(pa=101325.0)
|
||||
p2 = entropyk.Pressure(pa=50000.0)
|
||||
start = time.perf_counter()
|
||||
for _ in range(1000):
|
||||
_ = p1 + p2
|
||||
elapsed = time.perf_counter() - start
|
||||
assert elapsed < 0.1, f"1000 additions took {elapsed:.3f}s"
|
||||
|
||||
|
||||
class TestSystemBuildOverhead:
|
||||
"""Benchmark system construction overhead."""
|
||||
|
||||
def test_100_system_builds(self):
|
||||
"""Building 100 simple systems (4 components + 4 edges) should be fast."""
|
||||
start = time.perf_counter()
|
||||
for _ in range(100):
|
||||
system = entropyk.System()
|
||||
c = system.add_component(entropyk.Compressor())
|
||||
d = system.add_component(entropyk.Condenser())
|
||||
e = system.add_component(entropyk.ExpansionValve())
|
||||
v = system.add_component(entropyk.Evaporator())
|
||||
system.add_edge(c, d)
|
||||
system.add_edge(d, e)
|
||||
system.add_edge(e, v)
|
||||
system.add_edge(v, c)
|
||||
elapsed = time.perf_counter() - start
|
||||
assert elapsed < 1.0, f"100 system builds took {elapsed:.3f}s"
|
||||
248
bindings/python/tests/test_components.py
Normal file
248
bindings/python/tests/test_components.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""Entropyk — Unit Tests for Component Wrappers.
|
||||
|
||||
Tests for all component constructors, validation, and repr.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import entropyk
|
||||
|
||||
|
||||
class TestCompressor:
|
||||
"""Tests for Compressor component."""
|
||||
|
||||
def test_default(self):
|
||||
c = entropyk.Compressor()
|
||||
assert "Compressor" in repr(c)
|
||||
|
||||
def test_custom_params(self):
|
||||
c = entropyk.Compressor(speed_rpm=3600.0, efficiency=0.9, fluid="R410A")
|
||||
assert c.speed == 3600.0
|
||||
assert c.efficiency_value == pytest.approx(0.9)
|
||||
assert c.fluid_name == "R410A"
|
||||
|
||||
def test_negative_speed_raises(self):
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
entropyk.Compressor(speed_rpm=-1.0)
|
||||
|
||||
def test_negative_displacement_raises(self):
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
entropyk.Compressor(displacement=-1.0)
|
||||
|
||||
def test_invalid_efficiency_raises(self):
|
||||
with pytest.raises(ValueError, match="between"):
|
||||
entropyk.Compressor(efficiency=1.5)
|
||||
|
||||
def test_repr(self):
|
||||
c = entropyk.Compressor(speed_rpm=2900.0, efficiency=0.85, fluid="R134a")
|
||||
r = repr(c)
|
||||
assert "2900" in r
|
||||
assert "0.85" in r
|
||||
assert "R134a" in r
|
||||
|
||||
|
||||
class TestCondenser:
|
||||
"""Tests for Condenser component."""
|
||||
|
||||
def test_default(self):
|
||||
c = entropyk.Condenser()
|
||||
assert c.ua_value == pytest.approx(5000.0)
|
||||
|
||||
def test_custom_ua(self):
|
||||
c = entropyk.Condenser(ua=10000.0)
|
||||
assert c.ua_value == pytest.approx(10000.0)
|
||||
|
||||
def test_negative_ua_raises(self):
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
entropyk.Condenser(ua=-1.0)
|
||||
|
||||
def test_repr(self):
|
||||
c = entropyk.Condenser(ua=5000.0)
|
||||
assert "Condenser" in repr(c)
|
||||
assert "5000" in repr(c)
|
||||
|
||||
|
||||
class TestEvaporator:
|
||||
"""Tests for Evaporator component."""
|
||||
|
||||
def test_default(self):
|
||||
e = entropyk.Evaporator()
|
||||
assert e.ua_value == pytest.approx(3000.0)
|
||||
|
||||
def test_custom_ua(self):
|
||||
e = entropyk.Evaporator(ua=8000.0)
|
||||
assert e.ua_value == pytest.approx(8000.0)
|
||||
|
||||
def test_negative_ua_raises(self):
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
entropyk.Evaporator(ua=-1.0)
|
||||
|
||||
def test_repr(self):
|
||||
e = entropyk.Evaporator(ua=3000.0)
|
||||
assert "Evaporator" in repr(e)
|
||||
|
||||
|
||||
class TestEconomizer:
|
||||
"""Tests for Economizer component."""
|
||||
|
||||
def test_default(self):
|
||||
e = entropyk.Economizer()
|
||||
assert "Economizer" in repr(e)
|
||||
|
||||
def test_custom_ua(self):
|
||||
e = entropyk.Economizer(ua=5000.0)
|
||||
assert "5000" in repr(e)
|
||||
|
||||
def test_negative_ua_raises(self):
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
entropyk.Economizer(ua=-1.0)
|
||||
|
||||
|
||||
class TestExpansionValve:
|
||||
"""Tests for ExpansionValve component."""
|
||||
|
||||
def test_default(self):
|
||||
v = entropyk.ExpansionValve()
|
||||
assert v.fluid_name == "R134a"
|
||||
assert v.opening_value is None
|
||||
|
||||
def test_with_opening(self):
|
||||
v = entropyk.ExpansionValve(opening=0.5)
|
||||
assert v.opening_value == pytest.approx(0.5)
|
||||
|
||||
def test_invalid_opening_raises(self):
|
||||
with pytest.raises(ValueError, match="between"):
|
||||
entropyk.ExpansionValve(opening=1.5)
|
||||
|
||||
def test_repr(self):
|
||||
v = entropyk.ExpansionValve(fluid="R410A", opening=0.8)
|
||||
assert "ExpansionValve" in repr(v)
|
||||
assert "R410A" in repr(v)
|
||||
|
||||
|
||||
class TestPipe:
|
||||
"""Tests for Pipe component."""
|
||||
|
||||
def test_default(self):
|
||||
p = entropyk.Pipe()
|
||||
assert "Pipe" in repr(p)
|
||||
|
||||
def test_custom_params(self):
|
||||
p = entropyk.Pipe(length=5.0, diameter=0.025)
|
||||
assert "5.00" in repr(p)
|
||||
|
||||
def test_negative_length_raises(self):
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
entropyk.Pipe(length=-1.0)
|
||||
|
||||
def test_negative_diameter_raises(self):
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
entropyk.Pipe(diameter=-0.01)
|
||||
|
||||
|
||||
class TestPump:
|
||||
"""Tests for Pump component."""
|
||||
|
||||
def test_default(self):
|
||||
p = entropyk.Pump()
|
||||
assert "Pump" in repr(p)
|
||||
|
||||
def test_negative_pressure_raises(self):
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
entropyk.Pump(pressure_rise_pa=-100.0)
|
||||
|
||||
def test_invalid_efficiency_raises(self):
|
||||
with pytest.raises(ValueError, match="between"):
|
||||
entropyk.Pump(efficiency=2.0)
|
||||
|
||||
|
||||
class TestFan:
|
||||
"""Tests for Fan component."""
|
||||
|
||||
def test_default(self):
|
||||
f = entropyk.Fan()
|
||||
assert "Fan" in repr(f)
|
||||
|
||||
def test_negative_pressure_raises(self):
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
entropyk.Fan(pressure_rise_pa=-100.0)
|
||||
|
||||
|
||||
class TestFlowSplitter:
|
||||
"""Tests for FlowSplitter component."""
|
||||
|
||||
def test_default(self):
|
||||
s = entropyk.FlowSplitter()
|
||||
assert "FlowSplitter" in repr(s)
|
||||
|
||||
def test_custom_outlets(self):
|
||||
s = entropyk.FlowSplitter(n_outlets=3)
|
||||
assert "3" in repr(s)
|
||||
|
||||
def test_too_few_outlets_raises(self):
|
||||
with pytest.raises(ValueError, match=">="):
|
||||
entropyk.FlowSplitter(n_outlets=1)
|
||||
|
||||
|
||||
class TestFlowMerger:
|
||||
"""Tests for FlowMerger component."""
|
||||
|
||||
def test_default(self):
|
||||
m = entropyk.FlowMerger()
|
||||
assert "FlowMerger" in repr(m)
|
||||
|
||||
def test_custom_inlets(self):
|
||||
m = entropyk.FlowMerger(n_inlets=4)
|
||||
assert "4" in repr(m)
|
||||
|
||||
def test_too_few_inlets_raises(self):
|
||||
with pytest.raises(ValueError, match=">="):
|
||||
entropyk.FlowMerger(n_inlets=1)
|
||||
|
||||
|
||||
class TestFlowSource:
|
||||
"""Tests for FlowSource component."""
|
||||
|
||||
def test_default(self):
|
||||
s = entropyk.FlowSource()
|
||||
assert "FlowSource" in repr(s)
|
||||
|
||||
def test_custom(self):
|
||||
s = entropyk.FlowSource(pressure_pa=200000.0, temperature_k=350.0)
|
||||
assert "200000" in repr(s)
|
||||
|
||||
def test_negative_pressure_raises(self):
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
entropyk.FlowSource(pressure_pa=-1.0)
|
||||
|
||||
|
||||
class TestFlowSink:
|
||||
"""Tests for FlowSink component."""
|
||||
|
||||
def test_default(self):
|
||||
s = entropyk.FlowSink()
|
||||
assert "FlowSink" in repr(s)
|
||||
|
||||
|
||||
class TestOperationalState:
|
||||
"""Tests for OperationalState enum."""
|
||||
|
||||
def test_on(self):
|
||||
s = entropyk.OperationalState("on")
|
||||
assert str(s) == "On"
|
||||
|
||||
def test_off(self):
|
||||
s = entropyk.OperationalState("off")
|
||||
assert str(s) == "Off"
|
||||
|
||||
def test_bypass(self):
|
||||
s = entropyk.OperationalState("bypass")
|
||||
assert str(s) == "Bypass"
|
||||
|
||||
def test_invalid_raises(self):
|
||||
with pytest.raises(ValueError, match="one of"):
|
||||
entropyk.OperationalState("invalid")
|
||||
|
||||
def test_eq(self):
|
||||
s1 = entropyk.OperationalState("on")
|
||||
s2 = entropyk.OperationalState("on")
|
||||
assert s1 == s2
|
||||
96
bindings/python/tests/test_errors.py
Normal file
96
bindings/python/tests/test_errors.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""Entropyk — Unit Tests for Exception Hierarchy.
|
||||
|
||||
Tests that all exception types exist, inherit correctly, and carry messages.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import entropyk
|
||||
|
||||
|
||||
class TestExceptionHierarchy:
|
||||
"""Tests for Python exception class hierarchy."""
|
||||
|
||||
def test_entropyk_error_exists(self):
|
||||
assert hasattr(entropyk, "EntropykError")
|
||||
assert issubclass(entropyk.EntropykError, Exception)
|
||||
|
||||
def test_solver_error_inherits(self):
|
||||
assert issubclass(entropyk.SolverError, entropyk.EntropykError)
|
||||
|
||||
def test_timeout_error_inherits(self):
|
||||
assert issubclass(entropyk.TimeoutError, entropyk.SolverError)
|
||||
|
||||
def test_control_saturation_error_inherits(self):
|
||||
assert issubclass(entropyk.ControlSaturationError, entropyk.SolverError)
|
||||
|
||||
def test_fluid_error_inherits(self):
|
||||
assert issubclass(entropyk.FluidError, entropyk.EntropykError)
|
||||
|
||||
def test_component_error_inherits(self):
|
||||
assert issubclass(entropyk.ComponentError, entropyk.EntropykError)
|
||||
|
||||
def test_topology_error_inherits(self):
|
||||
assert issubclass(entropyk.TopologyError, entropyk.EntropykError)
|
||||
|
||||
def test_validation_error_inherits(self):
|
||||
assert issubclass(entropyk.ValidationError, entropyk.EntropykError)
|
||||
|
||||
|
||||
class TestExceptionMessages:
|
||||
"""Tests that exceptions carry descriptive messages."""
|
||||
|
||||
def test_entropyk_error_message(self):
|
||||
err = entropyk.EntropykError("test message")
|
||||
assert str(err) == "test message"
|
||||
|
||||
def test_solver_error_message(self):
|
||||
err = entropyk.SolverError("convergence failed")
|
||||
assert "convergence failed" in str(err)
|
||||
|
||||
def test_timeout_error_message(self):
|
||||
err = entropyk.TimeoutError("timed out after 5s")
|
||||
assert "timed out" in str(err)
|
||||
|
||||
def test_fluid_error_message(self):
|
||||
err = entropyk.FluidError("R134a not found")
|
||||
assert "R134a" in str(err)
|
||||
|
||||
def test_topology_error_message(self):
|
||||
err = entropyk.TopologyError("graph cycle detected")
|
||||
assert "cycle" in str(err)
|
||||
|
||||
|
||||
class TestExceptionCatching:
|
||||
"""Tests that exceptions can be caught at different hierarchy levels."""
|
||||
|
||||
def test_catch_solver_as_entropyk(self):
|
||||
with pytest.raises(entropyk.EntropykError):
|
||||
raise entropyk.SolverError("test")
|
||||
|
||||
def test_catch_timeout_as_solver(self):
|
||||
with pytest.raises(entropyk.SolverError):
|
||||
raise entropyk.TimeoutError("test")
|
||||
|
||||
def test_catch_timeout_as_entropyk(self):
|
||||
with pytest.raises(entropyk.EntropykError):
|
||||
raise entropyk.TimeoutError("test")
|
||||
|
||||
def test_catch_fluid_as_entropyk(self):
|
||||
with pytest.raises(entropyk.EntropykError):
|
||||
raise entropyk.FluidError("test")
|
||||
|
||||
def test_catch_component_as_entropyk(self):
|
||||
with pytest.raises(entropyk.EntropykError):
|
||||
raise entropyk.ComponentError("test")
|
||||
|
||||
def test_timeout_not_caught_as_fluid(self):
|
||||
"""TimeoutError should NOT be caught by FluidError."""
|
||||
with pytest.raises(entropyk.TimeoutError):
|
||||
raise entropyk.TimeoutError("test")
|
||||
# Verify it doesn't match FluidError
|
||||
try:
|
||||
raise entropyk.TimeoutError("test")
|
||||
except entropyk.FluidError:
|
||||
pytest.fail("TimeoutError should not be caught by FluidError")
|
||||
except entropyk.TimeoutError:
|
||||
pass # Expected
|
||||
72
bindings/python/tests/test_numpy.py
Normal file
72
bindings/python/tests/test_numpy.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Entropyk — NumPy / Buffer Protocol Tests.
|
||||
|
||||
Tests for zero-copy state vector access and NumPy integration.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import entropyk
|
||||
|
||||
# numpy may not be installed in test env — skip gracefully
|
||||
numpy = pytest.importorskip("numpy")
|
||||
|
||||
|
||||
class TestStateVectorNumpy:
|
||||
"""Tests for state vector as NumPy array."""
|
||||
|
||||
def test_state_vector_to_numpy(self):
|
||||
"""ConvergedState.state_vector returns a list convertible to np array."""
|
||||
# Build a minimal system so we can get a state vector length
|
||||
system = entropyk.System()
|
||||
system.add_component(entropyk.Condenser())
|
||||
system.add_component(entropyk.Evaporator())
|
||||
system.add_edge(0, 1)
|
||||
system.add_edge(1, 0)
|
||||
system.finalize()
|
||||
|
||||
# The state_vector_len should be > 0 after finalize
|
||||
svl = system.state_vector_len
|
||||
assert svl >= 0
|
||||
|
||||
def test_converged_state_vector_is_list(self):
|
||||
"""The state_vector attribute on ConvergedState should be a Python list
|
||||
of floats, convertible to numpy.array."""
|
||||
# We can't solve without real physics, but we can verify the accessor type
|
||||
# from the class itself
|
||||
assert hasattr(entropyk, "ConvergedState")
|
||||
|
||||
def test_numpy_array_from_list(self):
|
||||
"""Verify that a list of floats (as returned by state_vector) can be
|
||||
efficiently converted to a numpy array."""
|
||||
data = [1.0, 2.0, 3.0, 4.0, 5.0]
|
||||
arr = numpy.array(data, dtype=numpy.float64)
|
||||
assert arr.shape == (5,)
|
||||
assert arr.dtype == numpy.float64
|
||||
numpy.testing.assert_array_almost_equal(arr, data)
|
||||
|
||||
|
||||
class TestTypesWithNumpy:
|
||||
"""Tests for using core types with NumPy."""
|
||||
|
||||
def test_pressure_float_in_numpy(self):
|
||||
"""Pressure can be used as a float value in numpy operations."""
|
||||
p = entropyk.Pressure(bar=1.0)
|
||||
arr = numpy.array([float(p)], dtype=numpy.float64)
|
||||
assert arr[0] == pytest.approx(100000.0)
|
||||
|
||||
def test_temperature_float_in_numpy(self):
|
||||
"""Temperature can be used as a float value in numpy operations."""
|
||||
t = entropyk.Temperature(celsius=25.0)
|
||||
arr = numpy.array([float(t)], dtype=numpy.float64)
|
||||
assert arr[0] == pytest.approx(298.15)
|
||||
|
||||
def test_enthalpy_float_in_numpy(self):
|
||||
"""Enthalpy can be used as a float value in numpy operations."""
|
||||
h = entropyk.Enthalpy(kj_per_kg=250.0)
|
||||
arr = numpy.array([float(h)], dtype=numpy.float64)
|
||||
assert arr[0] == pytest.approx(250000.0)
|
||||
|
||||
def test_massflow_float_in_numpy(self):
|
||||
"""MassFlow can be used as a float value in numpy operations."""
|
||||
m = entropyk.MassFlow(kg_per_s=0.5)
|
||||
arr = numpy.array([float(m)], dtype=numpy.float64)
|
||||
assert arr[0] == pytest.approx(0.5)
|
||||
147
bindings/python/tests/test_solver.py
Normal file
147
bindings/python/tests/test_solver.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""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)
|
||||
|
||||
|
||||
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
|
||||
208
bindings/python/tests/test_types.py
Normal file
208
bindings/python/tests/test_types.py
Normal file
@@ -0,0 +1,208 @@
|
||||
"""Entropyk — Unit Tests for Core Physical Types.
|
||||
|
||||
Tests for Pressure, Temperature, Enthalpy, and MassFlow wrappers.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import entropyk
|
||||
|
||||
|
||||
class TestPressure:
|
||||
"""Tests for Pressure type."""
|
||||
|
||||
def test_from_pa(self):
|
||||
p = entropyk.Pressure(pa=101325.0)
|
||||
assert p.to_pascals() == pytest.approx(101325.0)
|
||||
|
||||
def test_from_bar(self):
|
||||
p = entropyk.Pressure(bar=1.01325)
|
||||
assert p.to_pascals() == pytest.approx(101325.0)
|
||||
|
||||
def test_from_kpa(self):
|
||||
p = entropyk.Pressure(kpa=101.325)
|
||||
assert p.to_pascals() == pytest.approx(101325.0)
|
||||
|
||||
def test_from_psi(self):
|
||||
p = entropyk.Pressure(psi=14.696)
|
||||
assert p.to_pascals() == pytest.approx(101325.0, rel=1e-3)
|
||||
|
||||
def test_to_bar(self):
|
||||
p = entropyk.Pressure(pa=100000.0)
|
||||
assert p.to_bar() == pytest.approx(1.0)
|
||||
|
||||
def test_to_kpa(self):
|
||||
p = entropyk.Pressure(pa=1000.0)
|
||||
assert p.to_kpa() == pytest.approx(1.0)
|
||||
|
||||
def test_float(self):
|
||||
p = entropyk.Pressure(pa=101325.0)
|
||||
assert float(p) == pytest.approx(101325.0)
|
||||
|
||||
def test_repr(self):
|
||||
p = entropyk.Pressure(bar=1.0)
|
||||
assert "Pressure" in repr(p)
|
||||
assert "bar" in repr(p)
|
||||
|
||||
def test_str(self):
|
||||
p = entropyk.Pressure(pa=100.0)
|
||||
assert "Pa" in str(p)
|
||||
|
||||
def test_eq(self):
|
||||
p1 = entropyk.Pressure(bar=1.0)
|
||||
p2 = entropyk.Pressure(bar=1.0)
|
||||
assert p1 == p2
|
||||
|
||||
def test_add(self):
|
||||
p1 = entropyk.Pressure(pa=100.0)
|
||||
p2 = entropyk.Pressure(pa=200.0)
|
||||
result = p1 + p2
|
||||
assert float(result) == pytest.approx(300.0)
|
||||
|
||||
def test_sub(self):
|
||||
p1 = entropyk.Pressure(pa=300.0)
|
||||
p2 = entropyk.Pressure(pa=100.0)
|
||||
result = p1 - p2
|
||||
assert float(result) == pytest.approx(200.0)
|
||||
|
||||
def test_multiple_kwargs_raises(self):
|
||||
with pytest.raises(ValueError, match="exactly one"):
|
||||
entropyk.Pressure(pa=100.0, bar=1.0)
|
||||
|
||||
def test_no_kwargs_raises(self):
|
||||
with pytest.raises(ValueError, match="exactly one"):
|
||||
entropyk.Pressure()
|
||||
|
||||
|
||||
class TestTemperature:
|
||||
"""Tests for Temperature type."""
|
||||
|
||||
def test_from_kelvin(self):
|
||||
t = entropyk.Temperature(kelvin=300.0)
|
||||
assert t.to_kelvin() == pytest.approx(300.0)
|
||||
|
||||
def test_from_celsius(self):
|
||||
t = entropyk.Temperature(celsius=25.0)
|
||||
assert t.to_kelvin() == pytest.approx(298.15)
|
||||
|
||||
def test_from_fahrenheit(self):
|
||||
t = entropyk.Temperature(fahrenheit=77.0)
|
||||
assert t.to_celsius() == pytest.approx(25.0)
|
||||
|
||||
def test_to_celsius(self):
|
||||
t = entropyk.Temperature(kelvin=273.15)
|
||||
assert t.to_celsius() == pytest.approx(0.0)
|
||||
|
||||
def test_to_fahrenheit(self):
|
||||
t = entropyk.Temperature(celsius=100.0)
|
||||
assert t.to_fahrenheit() == pytest.approx(212.0)
|
||||
|
||||
def test_float(self):
|
||||
t = entropyk.Temperature(kelvin=300.0)
|
||||
assert float(t) == pytest.approx(300.0)
|
||||
|
||||
def test_repr(self):
|
||||
t = entropyk.Temperature(celsius=25.0)
|
||||
assert "Temperature" in repr(t)
|
||||
|
||||
def test_eq(self):
|
||||
t1 = entropyk.Temperature(celsius=25.0)
|
||||
t2 = entropyk.Temperature(celsius=25.0)
|
||||
assert t1 == t2
|
||||
|
||||
def test_add(self):
|
||||
t1 = entropyk.Temperature(kelvin=100.0)
|
||||
t2 = entropyk.Temperature(kelvin=200.0)
|
||||
result = t1 + t2
|
||||
assert float(result) == pytest.approx(300.0)
|
||||
|
||||
def test_sub(self):
|
||||
t1 = entropyk.Temperature(kelvin=300.0)
|
||||
t2 = entropyk.Temperature(kelvin=100.0)
|
||||
result = t1 - t2
|
||||
assert float(result) == pytest.approx(200.0)
|
||||
|
||||
def test_multiple_kwargs_raises(self):
|
||||
with pytest.raises(ValueError, match="exactly one"):
|
||||
entropyk.Temperature(kelvin=300.0, celsius=25.0)
|
||||
|
||||
|
||||
class TestEnthalpy:
|
||||
"""Tests for Enthalpy type."""
|
||||
|
||||
def test_from_j_per_kg(self):
|
||||
h = entropyk.Enthalpy(j_per_kg=250000.0)
|
||||
assert h.to_j_per_kg() == pytest.approx(250000.0)
|
||||
|
||||
def test_from_kj_per_kg(self):
|
||||
h = entropyk.Enthalpy(kj_per_kg=250.0)
|
||||
assert h.to_j_per_kg() == pytest.approx(250000.0)
|
||||
|
||||
def test_to_kj_per_kg(self):
|
||||
h = entropyk.Enthalpy(j_per_kg=250000.0)
|
||||
assert h.to_kj_per_kg() == pytest.approx(250.0)
|
||||
|
||||
def test_float(self):
|
||||
h = entropyk.Enthalpy(j_per_kg=250000.0)
|
||||
assert float(h) == pytest.approx(250000.0)
|
||||
|
||||
def test_repr(self):
|
||||
h = entropyk.Enthalpy(kj_per_kg=250.0)
|
||||
assert "Enthalpy" in repr(h)
|
||||
|
||||
def test_eq(self):
|
||||
h1 = entropyk.Enthalpy(kj_per_kg=250.0)
|
||||
h2 = entropyk.Enthalpy(kj_per_kg=250.0)
|
||||
assert h1 == h2
|
||||
|
||||
def test_add(self):
|
||||
h1 = entropyk.Enthalpy(j_per_kg=100.0)
|
||||
h2 = entropyk.Enthalpy(j_per_kg=200.0)
|
||||
result = h1 + h2
|
||||
assert float(result) == pytest.approx(300.0)
|
||||
|
||||
def test_sub(self):
|
||||
h1 = entropyk.Enthalpy(j_per_kg=300.0)
|
||||
h2 = entropyk.Enthalpy(j_per_kg=100.0)
|
||||
result = h1 - h2
|
||||
assert float(result) == pytest.approx(200.0)
|
||||
|
||||
|
||||
class TestMassFlow:
|
||||
"""Tests for MassFlow type."""
|
||||
|
||||
def test_from_kg_per_s(self):
|
||||
m = entropyk.MassFlow(kg_per_s=0.5)
|
||||
assert m.to_kg_per_s() == pytest.approx(0.5)
|
||||
|
||||
def test_from_g_per_s(self):
|
||||
m = entropyk.MassFlow(g_per_s=500.0)
|
||||
assert m.to_kg_per_s() == pytest.approx(0.5)
|
||||
|
||||
def test_to_g_per_s(self):
|
||||
m = entropyk.MassFlow(kg_per_s=0.5)
|
||||
assert m.to_g_per_s() == pytest.approx(500.0)
|
||||
|
||||
def test_float(self):
|
||||
m = entropyk.MassFlow(kg_per_s=0.5)
|
||||
assert float(m) == pytest.approx(0.5)
|
||||
|
||||
def test_repr(self):
|
||||
m = entropyk.MassFlow(kg_per_s=0.5)
|
||||
assert "MassFlow" in repr(m)
|
||||
|
||||
def test_eq(self):
|
||||
m1 = entropyk.MassFlow(kg_per_s=0.5)
|
||||
m2 = entropyk.MassFlow(kg_per_s=0.5)
|
||||
assert m1 == m2
|
||||
|
||||
def test_add(self):
|
||||
m1 = entropyk.MassFlow(kg_per_s=0.1)
|
||||
m2 = entropyk.MassFlow(kg_per_s=0.2)
|
||||
result = m1 + m2
|
||||
assert float(result) == pytest.approx(0.3)
|
||||
|
||||
def test_sub(self):
|
||||
m1 = entropyk.MassFlow(kg_per_s=0.5)
|
||||
m2 = entropyk.MassFlow(kg_per_s=0.2)
|
||||
result = m1 - m2
|
||||
assert float(result) == pytest.approx(0.3)
|
||||
Reference in New Issue
Block a user