diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c26102839..7c7b1ad4a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,6 +23,18 @@ jobs: - name: Run tests run: cargo test --release -p ${{ matrix.crate }} + test_python_bindings: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: 3.14 + - name: Build + run: cargo build --release --manifest-path py-feos/Cargo.toml + - name: Run tests + run: cargo test --release --manifest-path py-feos/Cargo.toml + test_models: runs-on: ubuntu-latest strategy: diff --git a/CHANGELOG.md b/CHANGELOG.md index 93e16be91..951073bbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -## Fixed +### Added +- Added `py-feos` tests to GitHub Actions and moved `pyo3/extension-module` feature to `pyproject.toml`. [#334](https://github.com/feos-org/feos/pull/334) + +### Fixed +- Fixed `None` transformation when binary parameters are provided to `PyParameters` to properly raise errors. [#334](https://github.com/feos-org/feos/pull/334) - Fixed the calculation of critical points and tp-flashes when one or more of the mixture components have zero composition. [#331](https://github.com/feos-org/feos/pull/331) ### Packaging diff --git a/crates/feos-core/src/cubic.rs b/crates/feos-core/src/cubic.rs index e75870f20..562422b05 100644 --- a/crates/feos-core/src/cubic.rs +++ b/crates/feos-core/src/cubic.rs @@ -36,8 +36,21 @@ impl PengRobinsonRecord { } } +/// Peng-Robinson binary interaction parameters. +#[derive(Serialize, Deserialize, Clone, Copy, Default, Debug)] +pub struct PengRobinsonBinaryRecord { + /// Binary interaction parameter + pub k_ij: f64, +} + +impl PengRobinsonBinaryRecord { + pub fn new(k_ij: f64) -> Self { + Self { k_ij } + } +} + /// Peng-Robinson parameters for one ore more substances. -pub type PengRobinsonParameters = Parameters; +pub type PengRobinsonParameters = Parameters; impl PengRobinsonParameters { /// Build a simple parameter set without binary interaction parameters. @@ -87,7 +100,7 @@ impl PengRobinson { /// Create a new equation of state from a set of parameters. pub fn new(parameters: PengRobinsonParameters) -> Self { let [tc, pc, ac] = parameters.collate(|r| [r.tc, r.pc, r.acentric_factor]); - let [k_ij] = parameters.collate_binary(|&br| [br]); + let [k_ij] = parameters.collate_binary(|&br| [br.k_ij]); let a = 0.45724 * KB_A3 * tc.component_mul(&tc).component_div(&pc); let b = 0.07780 * KB_A3 * &tc.component_div(&pc); diff --git a/py-feos/Cargo.toml b/py-feos/Cargo.toml index 9affe6b99..45c22ea63 100644 --- a/py-feos/Cargo.toml +++ b/py-feos/Cargo.toml @@ -16,7 +16,6 @@ crate-type = ["cdylib"] [dependencies] pyo3 = { version = "0.27", features = [ - "extension-module", "abi3-py310", "multiple-pymethods", "indexmap" @@ -69,3 +68,6 @@ all_models = [ "multiparameter", "ad" ] + +[dev-dependencies] +tempfile = "3.24.0" diff --git a/py-feos/pyproject.toml b/py-feos/pyproject.toml index e43a7ca27..fdd628cfc 100644 --- a/py-feos/pyproject.toml +++ b/py-feos/pyproject.toml @@ -19,4 +19,4 @@ requires = ["maturin>=1.0,<2.0"] build-backend = "maturin" [tool.maturin] -features = ["all_models"] \ No newline at end of file +features = ["pyo3/extension-module", "all_models"] diff --git a/py-feos/src/parameter/mod.rs b/py-feos/src/parameter/mod.rs index 5f67e78b1..115f8f79a 100644 --- a/py-feos/src/parameter/mod.rs +++ b/py-feos/src/parameter/mod.rs @@ -44,7 +44,31 @@ impl PyParameters { let binary_records = self .binary_records .into_iter() - .map(|r| Ok(serde_json::from_value(serde_json::to_value(r)?)?)) + .map(|r| { + // Explicitly parse model record + // Needed for returning error if parsing fails instead of returning None + let model_record = match r.model_record { + Some(v) => Some(serde_json::from_value::(v)?), + None => None, + }; + + let association_sites = r + .association_sites + .into_iter() + .map(|site| { + Ok(serde_json::from_value::>( + serde_json::to_value(site)?, + )?) + }) + .collect::, PyFeosError>>()?; + + Ok(BinaryRecord { + id1: r.id1, + id2: r.id2, + model_record, + association_sites, + }) + }) .collect::>()?; Ok(Parameters::new(pure_records, binary_records).map_err(PyFeosError::from)?) } @@ -720,3 +744,68 @@ impl PyGcParameters { ) } } + +// Note: PyErr requires pyo3::Python::initialize(). +// If you see weird messages in your test-output (lifetime issue with PyO3), +// consider initializing the Python interpreter. As long as no errors are raised, we don't need it. +// +// To check tempfiles directory, add: +// println!("Storing tempfiles in: {:?}", std::env::temp_dir()); +#[cfg(test)] +mod test { + use super::*; + use feos_core::cubic::{PengRobinsonBinaryRecord, PengRobinsonParameters, PengRobinsonRecord}; + use tempfile::NamedTempFile; + + // Helper trait to extract path as String + trait TempFileExt { + fn path_string(&self) -> String; + } + + impl TempFileExt for NamedTempFile { + fn path_string(&self) -> String { + self.path().to_str().unwrap().to_string() + } + } + + #[test] + fn test_pr_binary_parameter_from_json() { + pyo3::Python::initialize(); + + // Create pure substance parameter file + let params: Vec> = vec![ + PureRecord::new( + Identifier::new(None, Some("1"), None, None, None, None), + 1.0, + PengRobinsonRecord::new(1.0, 1.0, 1.0), + ), + PureRecord::new( + Identifier::new(None, Some("2"), None, None, None, None), + 2.0, + PengRobinsonRecord::new(2.0, 2.0, 2.0), + ), + ]; + // Create pure parameter file + let pure_path = NamedTempFile::new().unwrap(); + serde_json::to_writer(&pure_path, ¶ms).unwrap(); + + let binary_params: Vec> = + vec![BinaryRecord::new( + Identifier::new(None, Some("1"), None, None, None, None), + Identifier::new(None, Some("2"), None, None, None, None), + Some(PengRobinsonBinaryRecord::new(2.0)), + )]; + // Create binary parameter file + let binary_path = NamedTempFile::new().unwrap(); + serde_json::to_writer(&binary_path, &binary_params).unwrap(); + + let py_parameters = PyParameters::from_json( + vec!["1".to_string(), "2".to_string()], + pure_path.path_string(), + Some(binary_path.path_string()), + PyIdentifierOption::Name, + ); + let parameter: PengRobinsonParameters = py_parameters.unwrap().try_convert().unwrap(); + assert!(parameter.binary[0].model_record.k_ij == 2.0); + } +}