Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.4.0] - 2026-07-15
### Added
- Included molar weight in `ChemicalRecord`.
- Added `ripopt` as alternative to `ipopt` for solving the NLP subproblems.

### Packaging
- Updated to feos v0.10.0.

## [0.3.3] - 2025-04-14
### Fixed
- Fixed a compatibility problem with the newest version of `num-dual`. [#7](https://github.com/feos-org/feos-campd/pull/7)
- Fixed a compatibility problem with the newest version of `num-dual`.

## [0.3.2] - 2025-01-27
### Fixed
Expand Down
28 changes: 15 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "feos-campd"
version = "0.3.3"
version = "0.4.0"
authors = ["Philipp Rehner <prehner@ethz.ch>"]
edition = "2021"
readme = "README.md"
Expand All @@ -12,22 +12,24 @@ keywords = ["process_engineering", "molecular_design", "optimization"]
categories = ["science"]

[dependencies]
num-dual = { version = "0.11", features = ["linalg"] }
quantity = { version = "0.10", features = ["num-dual"] }
nalgebra = "0.33"
feos-core = "0.8"
ndarray = "0.16"
feos-ad = "0.2"
num-dual = "0.14"
quantity = "0.14"
nalgebra = "0.35"
feos = { version = "0.10", features = ["pcsaft", "gc_pcsaft"] }
num-traits = "0.2"
good_lp = { version = "1.11", default-features = false }
ipopt = "0.6"
ipopt-ad = "0.1"
serde = "1.0"
good_lp = { version = "1", default-features = false }
ipopt = { version = "0.6", optional = true }
ripopt = { version = "0.8", optional = true }
ipopt-ad = "0.3"
serde = "1"

[dev-dependencies]
approx = "0.5"
itertools = "0.14"
quantity = { version = "0.14", features = ["approx"] }
itertools = "0.15"

[features]
default = ["highs"]
default = ["highs", "ripopt"]
highs = ["good_lp/highs"]
ipopt = ["dep:ipopt", "ipopt-ad/ipopt"]
ripopt = ["dep:ripopt", "ipopt-ad/ripopt"]
45 changes: 27 additions & 18 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
use feos_core::EosError;
#[cfg(all(feature = "ipopt", feature = "ripopt"))]
compile_error!("Features 'ipopt' and 'ripopt' cannot be enabled at the same time.");

#[cfg(not(any(feature = "ipopt", feature = "ripopt")))]
compile_error!("Either feature 'ipopt' or 'ripopt' must be enabled.");

use feos::core::FeosError;
use good_lp::{Constraint, Variable};
use nalgebra::{SMatrix, SVector};
use num_dual::DualNum;
use quantity::MolarWeight;
use std::array;
use std::collections::{HashMap, HashSet};

Expand All @@ -13,18 +20,28 @@ pub use molecule::{CoMTCAMD, Disjunction, MolecularRepresentation, SuperMolecule
pub use process::{ContinuousVariable, ProcessModel};
pub use property::{GcPcSaftPropertyModel, PcSaftPropertyModel, PropertyModel};
pub use solver::{
GeneralConstraint, MixedIntegerNonLinearProgram, OptimizationResult, OuterApproximation,
GeneralConstraint, MixedIntegerNonLinearProgram, OptimizationOptions, OptimizationResult,
OuterApproximation,
};

/// Input for group-contribution models that allows for derivatives.
pub struct ChemicalRecord<D> {
pub groups: HashMap<&'static str, D>,
pub bonds: HashMap<[&'static str; 2], D>,
pub molar_weight: MolarWeight<D>,
}

impl<D> ChemicalRecord<D> {
pub fn new(groups: HashMap<&'static str, D>, bonds: HashMap<[&'static str; 2], D>) -> Self {
Self { groups, bonds }
pub fn new(
groups: HashMap<&'static str, D>,
bonds: HashMap<[&'static str; 2], D>,
molar_weight: MolarWeight<D>,
) -> Self {
Self {
groups,
bonds,
molar_weight,
}
}
}

Expand All @@ -49,13 +66,13 @@ impl<M, R, P> IntegratedDesign<M, R, P> {
impl<
M: MolecularRepresentation<N_Y>,
R: PropertyModel<N>,
P: ProcessModel<R::EquationOfState, N_X, N>,
P: ProcessModel<N_X, N>,
const N_X: usize,
const N_Y: usize,
const N: usize,
> MixedIntegerNonLinearProgram<N_X, N_Y, N> for IntegratedDesign<M, R, P>
{
type Error = EosError;
type Error = FeosError;

fn x_variables(&self) -> SVector<(f64, f64, f64), N_X> {
SVector::from(self.process.variables().map(|v| (v.lobnd, v.upbnd, v.init)))
Expand All @@ -81,10 +98,10 @@ impl<
&self,
x: SVector<D, N_X>,
y: SMatrix<D, N_Y, N>,
) -> Result<(D, Vec<D>), EosError> {
) -> Result<(D, Vec<D>), FeosError> {
let y_set: HashSet<_> = y.data.0.iter().map(|y| y.map(|y| y.re() as i32)).collect();
if y_set.len() != N {
Err(EosError::IncompatibleComponents(N, y_set.len()))
Err(FeosError::IncompatibleComponents(N, y_set.len()))
} else {
let cr = y.data.0.map(|y| self.molecule.build_molecule(y));
let eos = self.property.build_eos(cr.each_ref());
Expand Down Expand Up @@ -170,11 +187,7 @@ mod test {
result.x.data.0[0],
MOLECULE.smiles(&result.y.data.0[0])
);
assert_relative_eq!(
result.objective.0,
-0.4378352970105434,
max_relative = 1e-10
);
assert_relative_eq!(result.objective.0, -0.4378352970105434, max_relative = 1e-8);
}

#[test]
Expand Down Expand Up @@ -207,11 +220,7 @@ mod test {
result.x.data.0[0],
molecule.smiles(&result.y.data.0[0])
);
assert_relative_eq!(
result.objective.0,
-0.4378352970105434,
max_relative = 1e-10
);
assert_relative_eq!(result.objective.0, -0.4378352970105434, max_relative = 1e-8);
}

#[test]
Expand Down
75 changes: 44 additions & 31 deletions src/molecule/comt_camd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,59 @@ use super::MolecularRepresentation;
use crate::ChemicalRecord;
use good_lp::{constraint, Constraint, Expression, Variable};
use num_dual::DualNum;
use quantity::MolarWeight;
use std::collections::HashMap;
use std::sync::LazyLock;

struct Group {
name: &'static str,
pub(super) struct Group {
pub name: &'static str,
open_bonds: usize,
n_max: i32,
heavy_atoms: usize,
heavy_atoms: i32,
pub molar_weight: f64,
}

impl Group {
const fn new(name: &'static str, open_bonds: usize, n_max: i32, heavy_atoms: usize) -> Self {
const fn new(name: &'static str, open_bonds: usize, n_max: i32, hcno: [i32; 4]) -> Self {
let [h, c, n, o] = hcno;
let heavy_atoms = c + n + o;
// values from PubChem https://pubchem.ncbi.nlm.nih.gov/ptable/atomic-mass/
let molar_weight =
h as f64 * 1.008 + c as f64 * 12.011 + n as f64 * 14.007 + o as f64 * 15.999;
Self {
name,
open_bonds,
n_max,
heavy_atoms,
molar_weight,
}
}
}

const N_GROUPS: usize = 22;
const GROUPS: [Group; N_GROUPS] = [
Group::new("CH3", 1, 10, 1),
Group::new("CH2", 2, 10, 1),
Group::new(">CH", 3, 10, 1),
Group::new(">C<", 4, 10, 1),
Group::new("=CH2", 1, 1, 1),
Group::new("=CH", 2, 2, 1),
Group::new("=C<", 3, 2, 1),
Group::new("C≡CH", 1, 1, 2),
Group::new("CH2_hex", 2, 6, 1),
Group::new("CH_hex", 3, 6, 1),
Group::new("CH2_pent", 2, 5, 1),
Group::new("CH_pent", 3, 5, 1),
Group::new("CH_arom", 2, 6, 1),
Group::new("C_arom", 3, 6, 1),
Group::new("CH=O", 1, 1, 2),
Group::new(">C=O", 2, 1, 2),
Group::new("OCH3", 1, 1, 2),
Group::new("OCH2", 2, 1, 2),
Group::new("HCOO", 1, 1, 3),
Group::new("COO", 2, 1, 3),
Group::new("OH", 1, 1, 1),
Group::new("NH2", 1, 1, 1),
pub(super) const GROUPS: [Group; N_GROUPS] = [
Group::new("CH3", 1, 10, [3, 1, 0, 0]),
Group::new("CH2", 2, 10, [2, 1, 0, 0]),
Group::new(">CH", 3, 10, [1, 1, 0, 0]),
Group::new(">C<", 4, 10, [0, 1, 0, 0]),
Group::new("=CH2", 1, 1, [2, 1, 0, 0]),
Group::new("=CH", 2, 2, [1, 1, 0, 0]),
Group::new("=C<", 3, 2, [0, 1, 0, 0]),
Group::new("C≡CH", 1, 1, [1, 2, 0, 0]),
Group::new("CH2_hex", 2, 6, [2, 1, 0, 0]),
Group::new("CH_hex", 3, 6, [1, 1, 0, 0]),
Group::new("CH2_pent", 2, 5, [2, 1, 0, 0]),
Group::new("CH_pent", 3, 5, [1, 1, 0, 0]),
Group::new("CH_arom", 2, 6, [1, 1, 0, 0]),
Group::new("C_arom", 3, 6, [0, 1, 0, 0]),
Group::new("CH=O", 1, 1, [1, 1, 0, 1]),
Group::new(">C=O", 2, 1, [0, 1, 0, 1]),
Group::new("OCH3", 1, 1, [3, 1, 0, 1]),
Group::new("OCH2", 2, 1, [2, 1, 0, 1]),
Group::new("HCOO", 1, 1, [1, 1, 0, 2]),
Group::new("COO", 2, 1, [0, 1, 0, 2]),
Group::new("OH", 1, 1, [1, 0, 0, 1]),
Group::new("NH2", 1, 1, [2, 0, 1, 0]),
];

struct Structure {
Expand Down Expand Up @@ -163,14 +171,19 @@ impl MolecularRepresentation<N_Y> for CoMTCAMD {
) -> ChemicalRecord<D> {
let mut group_counts = [D::zero(); N_GROUPS];
group_counts.copy_from_slice(&feature_variables[N_STRUCTURES..]);
let mut molar_weight = D::from(0.0);
let groups: HashMap<_, _> = GROUPS
.iter()
.zip(group_counts)
.map(|(g, c)| (g.name, c))
.map(|(g, c)| {
molar_weight += c * g.molar_weight;
(g.name, c)
})
.collect();
ChemicalRecord {
ChemicalRecord::new(
groups,
bonds: HashMap::new(),
}
HashMap::new(),
MolarWeight::new(molar_weight * 1e-3),
)
}
}
5 changes: 4 additions & 1 deletion src/molecule/disjunction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use super::{ChemicalRecord, MolecularRepresentation, SuperMolecule};
use crate::GeneralConstraint;
use good_lp::{constraint, Constraint, Expression, Variable};
use num_dual::DualNum;
use quantity::{Dimensionless, MolarWeight};
use std::collections::HashMap;

/// A combination of different molecule superstructures.
Expand Down Expand Up @@ -96,6 +97,7 @@ where

let mut groups = HashMap::new();
let mut bonds = HashMap::new();
let mut molar_weight = MolarWeight::new(D::from(0.0));
for (m, c) in self.0.iter().zip(c) {
let cr = m.build(y.to_vec());
cr.groups
Expand All @@ -104,7 +106,8 @@ where
cr.bonds
.into_iter()
.for_each(|(b, v)| *bonds.entry(b).or_insert(D::zero()) += v * c);
molar_weight += cr.molar_weight * Dimensionless::new(*c);
}
ChemicalRecord { groups, bonds }
ChemicalRecord::new(groups, bonds, molar_weight)
}
}
2 changes: 1 addition & 1 deletion src/molecule/polynomial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ impl<D: DualNum<f64> + Copy, const N: usize> Polynomial<D, N> {
self.0[0].into_iter().sum()
}

pub fn iter(&self) -> Iter<D> {
pub fn iter(&self) -> Iter<'_, D> {
self.0[0].iter()
}
}
Expand Down
Loading