feos-campd 0.2.0

Computer-aided molecular and process design using the FeOs framework.
Documentation
use crate::GcPcSaftPropertyModel;
use crate::{
    CoMTCAMD, OptimizationProblem, OptimizationResult, OuterApproximationAlgorithm,
    PcSaftPropertyModel,
};
use feos_core::EosError;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::wrap_pymodule;
use quantity::python::quantity as quantity_module;
use std::collections::HashMap;

mod eos;
mod molecule;
mod process;
mod property;

use eos::gc_pcsaft::{gc_pcsaft as gc_pcsaft_module, PyGcPcSaft};
use eos::pcsaft::{pcsaft as pcsaft_module, PyPcSaft};
use molecule::{PyCoMTCAMD, PySuperMolecule, SuperMolecules};
use process::PyProcessModel;
use property::{PyGcPcSaftPropertyModel, PyPcSaftPropertyModel};

#[pyclass(name = "OuterApproximationAlgorithm")]
#[derive(Clone, Copy)]
pub struct PyOuterApproximationAlgorithm(OuterApproximationAlgorithm);

#[pymethods]
#[allow(non_snake_case)]
impl PyOuterApproximationAlgorithm {
    #[staticmethod]
    fn DuranGrossmann(update_lower_bound: bool) -> Self {
        Self(OuterApproximationAlgorithm::DuranGrossmann(
            update_lower_bound,
        ))
    }

    #[classattr]
    fn FletcherLeyffer() -> Self {
        Self(OuterApproximationAlgorithm::FletcherLeyffer)
    }
}

#[pyclass(name = "OptimizationProblem")]
pub struct PyOptimizationProblem;

#[pymethods]
impl PyOptimizationProblem {
    #[staticmethod]
    fn pure(
        py: Python,
        molecule: &PyAny,
        property: &PyAny,
        process: PyProcessModel,
    ) -> PyResult<PyObject> {
        if let (Ok(comt_camd), Ok(pcsaft)) = (
            molecule.extract::<PyCoMTCAMD>(),
            property.extract::<PyPcSaftPropertyModel>(),
        ) {
            Ok(
                PyOptimizationProblem1(OptimizationProblem::new([comt_camd.0], pcsaft.0, process))
                    .into_py(py),
            )
        } else if let (Ok(supermolecule), Ok(pcsaft)) = (
            molecule.extract::<PySuperMolecule>(),
            property.extract::<PyPcSaftPropertyModel>(),
        ) {
            Ok(PyOptimizationProblem2(OptimizationProblem::new(
                [supermolecule.0],
                pcsaft.0,
                process,
            ))
            .into_py(py))
        } else if let (Ok(supermolecule), Ok(gc_pcsaft)) = (
            molecule.extract::<PySuperMolecule>(),
            property.extract::<PyGcPcSaftPropertyModel>(),
        ) {
            Ok(PyOptimizationProblem3(OptimizationProblem::new(
                [supermolecule.0],
                gc_pcsaft.0,
                process,
            ))
            .into_py(py))
        } else {
            Err(PyValueError::new_err("Ivalid arguments!"))
        }
    }

    #[staticmethod]
    fn binary(
        py: Python,
        molecule: &PyAny,
        property: &PyAny,
        process: PyProcessModel,
    ) -> PyResult<PyObject> {
        if let (Ok([m1, m2]), Ok(pcsaft)) = (
            molecule.extract::<[PyCoMTCAMD; 2]>(),
            property.extract::<PyPcSaftPropertyModel>(),
        ) {
            Ok(
                PyOptimizationProblem4(OptimizationProblem::new([m1.0, m2.0], pcsaft.0, process))
                    .into_py(py),
            )
        } else if let (Ok([m1, m2]), Ok(pcsaft)) = (
            molecule.extract::<[PySuperMolecule; 2]>(),
            property.extract::<PyPcSaftPropertyModel>(),
        ) {
            Ok(
                PyOptimizationProblem5(OptimizationProblem::new([m1.0, m2.0], pcsaft.0, process))
                    .into_py(py),
            )
        } else if let (Ok([m1, m2]), Ok(gc_pcsaft)) = (
            molecule.extract::<[PySuperMolecule; 2]>(),
            property.extract::<PyGcPcSaftPropertyModel>(),
        ) {
            Ok(
                PyOptimizationProblem6(OptimizationProblem::new(
                    [m1.0, m2.0],
                    gc_pcsaft.0,
                    process,
                ))
                .into_py(py),
            )
        } else {
            Err(PyValueError::new_err("Ivalid arguments!"))
        }
    }
}

macro_rules! impl_optimization_problem {
    ($py_optimization_problem:ident, $molecule:ident, $property_model:ident, $py_eos:ident, 1) => {
        #[pyclass(name = "OptimizationProblem")]
        pub struct $py_optimization_problem(
            OptimizationProblem<$molecule, $property_model, PyProcessModel, 1>,
        );

        #[pymethods]
        impl $py_optimization_problem {
            fn solve_fixed(
                &mut self,
                y_fixed: Vec<f64>,
                options: Option<&str>,
            ) -> PyResult<PyOptimizationResult> {
                Ok(PyOptimizationResult(
                    self.0
                        .solve_fixed(&[y_fixed], options)
                        .map_err(|_| EosError::NotConverged("Process optimization".into()))?,
                ))
            }

            fn solve_outer_approximation(
                &mut self,
                y_fixed: Vec<f64>,
                algorithm: PyOuterApproximationAlgorithm,
                options_nlp: Option<&str>,
                options_mip: Option<&str>,
            ) -> PyResult<PyOptimizationResult> {
                Ok(PyOptimizationResult(self.0.solve_outer_approximation(
                    [y_fixed],
                    algorithm.0,
                    options_nlp,
                    options_mip,
                )?))
            }

            fn outer_approximation_ranking(
                &mut self,
                y_fixed: Vec<f64>,
                algorithm: PyOuterApproximationAlgorithm,
                runs: usize,
                options_nlp: Option<&str>,
                options_mip: Option<&str>,
            ) {
                self.0.outer_approximation_ranking(
                    [y_fixed],
                    algorithm.0,
                    runs,
                    options_nlp,
                    options_mip,
                )
            }

            #[getter]
            fn get_solutions(&self) -> Vec<PyOptimizationResult> {
                let mut solutions: Vec<_> = self.0.solutions.iter().collect();
                solutions.sort_by(|s1, s2| s1.target.total_cmp(&s2.target));
                solutions
                    .into_iter()
                    .map(|result| PyOptimizationResult(result.clone()))
                    .collect()
            }

            fn build_eos(&self, result: PyOptimizationResult, py: Python) -> PyObject {
                $py_eos(self.0.build_eos(&result.0)).into_py(py)
            }
        }
    };
    ($py_optimization_problem:ident, $molecule:ident, $property_model:ident, $py_eos:ident, 2) => {
        #[pyclass(name = "OptimizationProblem")]
        pub struct $py_optimization_problem(
            OptimizationProblem<$molecule, $property_model, PyProcessModel, 2>,
        );

        #[pymethods]
        impl $py_optimization_problem {
            fn solve_fixed(
                &mut self,
                y_fixed: [Vec<f64>; 2],
                options: Option<&str>,
            ) -> PyResult<PyOptimizationResult> {
                Ok(PyOptimizationResult(
                    self.0
                        .solve_fixed(&y_fixed, options)
                        .map_err(|_| EosError::NotConverged("Process optimization".into()))?,
                ))
            }

            fn solve_outer_approximation(
                &mut self,
                y_fixed: [Vec<f64>; 2],
                algorithm: PyOuterApproximationAlgorithm,
                options_nlp: Option<&str>,
                options_mip: Option<&str>,
            ) -> PyResult<PyOptimizationResult> {
                Ok(PyOptimizationResult(self.0.solve_outer_approximation(
                    y_fixed,
                    algorithm.0,
                    options_nlp,
                    options_mip,
                )?))
            }

            fn outer_approximation_ranking(
                &mut self,
                y_fixed: [Vec<f64>; 2],
                algorithm: PyOuterApproximationAlgorithm,
                runs: usize,
                options_nlp: Option<&str>,
                options_mip: Option<&str>,
            ) {
                self.0.outer_approximation_ranking(
                    y_fixed,
                    algorithm.0,
                    runs,
                    options_nlp,
                    options_mip,
                )
            }

            #[getter]
            fn get_solutions(&self) -> Vec<PyOptimizationResult> {
                let mut solutions: Vec<_> = self.0.solutions.iter().collect();
                solutions.sort_by(|s1, s2| s1.target.total_cmp(&s2.target));
                solutions
                    .into_iter()
                    .map(|result| PyOptimizationResult(result.clone()))
                    .collect()
            }

            fn build_eos(&self, result: PyOptimizationResult, py: Python) -> PyObject {
                $py_eos(self.0.build_eos(&result.0)).into_py(py)
            }
        }
    };
}

impl_optimization_problem!(
    PyOptimizationProblem1,
    CoMTCAMD,
    PcSaftPropertyModel,
    PyPcSaft,
    1
);
impl_optimization_problem!(
    PyOptimizationProblem2,
    SuperMolecules,
    PcSaftPropertyModel,
    PyPcSaft,
    1
);
impl_optimization_problem!(
    PyOptimizationProblem3,
    SuperMolecules,
    GcPcSaftPropertyModel,
    PyGcPcSaft,
    1
);
impl_optimization_problem!(
    PyOptimizationProblem4,
    CoMTCAMD,
    PcSaftPropertyModel,
    PyPcSaft,
    2
);
impl_optimization_problem!(
    PyOptimizationProblem5,
    SuperMolecules,
    PcSaftPropertyModel,
    PyPcSaft,
    2
);
impl_optimization_problem!(
    PyOptimizationProblem6,
    SuperMolecules,
    GcPcSaftPropertyModel,
    PyGcPcSaft,
    2
);

#[pyclass(name = "PyOptimizationResult")]
#[derive(Clone)]
pub struct PyOptimizationResult(OptimizationResult);

#[pymethods]
impl PyOptimizationResult {
    #[getter]
    fn get_target(&self) -> f64 {
        self.0.target
    }

    #[getter]
    fn get_x(&self) -> Vec<f64> {
        self.0.x.clone()
    }

    #[getter]
    fn get_y(&self) -> Vec<usize> {
        self.0.y.clone()
    }

    #[getter]
    fn get_smiles(&self) -> Vec<String> {
        self.0.smiles.clone()
    }

    #[getter]
    fn get_groups(&self) -> Vec<HashMap<String, usize>> {
        self.0
            .smiles
            .iter()
            .map(|smiles| {
                let mut count = HashMap::new();
                smiles
                    .split(',')
                    .for_each(|g| *count.entry(g.to_string()).or_insert(0) += 1);
                count
            })
            .collect()
    }
}

#[pymodule]
pub fn feos_campd(py: Python<'_>, m: &PyModule) -> PyResult<()> {
    m.add("__version__", env!("CARGO_PKG_VERSION"))?;
    m.add_wrapped(wrap_pymodule!(quantity_module))?;
    m.add_wrapped(wrap_pymodule!(pcsaft_module))?;
    m.add_wrapped(wrap_pymodule!(gc_pcsaft_module))?;

    m.add_class::<PyProcessModel>()?;
    m.add_class::<PyCoMTCAMD>()?;
    m.add_class::<PySuperMolecule>()?;
    m.add_class::<PyPcSaftPropertyModel>()?;
    m.add_class::<PyGcPcSaftPropertyModel>()?;
    m.add_class::<PyOptimizationProblem>()?;
    m.add_class::<PyOuterApproximationAlgorithm>()?;

    set_path(py, m, "feos_campd.si", "quantity")?;
    set_path(py, m, "feos_campd.pcsaft", "pcsaft")?;
    set_path(py, m, "feos_campd.gc_pcsaft", "gc_pcsaft")?;
    Ok(())
}

fn set_path(py: Python<'_>, m: &PyModule, path: &str, module: &str) -> PyResult<()> {
    py.run(
        &format!(
            "\
import sys
sys.modules['{path}'] = {module}
    "
        ),
        None,
        Some(m.dict()),
    )
}