Skip to content

Commit 7a2b370

Browse files
committed
Split virial to module and add tests
1 parent fc8681f commit 7a2b370

File tree

3 files changed

+141
-96
lines changed

3 files changed

+141
-96
lines changed

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ pub mod icotable;
66
pub mod report;
77
pub mod structure;
88
pub mod table;
9+
mod virial;
10+
pub use virial::VirialCoeff;
911
extern crate pretty_env_logger;
1012
#[macro_use]
1113
extern crate log;

src/report.rs

Lines changed: 4 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,10 @@
1-
use crate::Sample;
1+
use crate::{Sample, VirialCoeff};
22
use coulomb::Vector3;
3-
use itertools::Itertools;
43
use nu_ansi_term::Color::{Red, Yellow};
5-
use num_traits::Inv;
6-
use physical_constants::AVOGADRO_CONSTANT;
74
use rgb::RGB8;
8-
use std::{
9-
f64::consts::PI,
10-
fs::File,
11-
io::Write,
12-
ops::{Add, Mul, Neg},
13-
path::PathBuf,
14-
};
5+
use std::{fs::File, io::Write, path::PathBuf};
156
use textplots::{Chart, ColorPlot, Shape};
167

17-
/// Helper struct for handling osmotic second virial coefficients, B2
18-
#[derive(Debug, Clone)]
19-
pub struct VirialCoeff {
20-
/// Osmotic second virial coefficient, B2 (ų)
21-
b2: f64,
22-
/// Distance of closest approach, σ (Å)
23-
sigma: f64,
24-
}
25-
26-
impl From<VirialCoeff> for f64 {
27-
fn from(v: VirialCoeff) -> f64 {
28-
v.b2
29-
}
30-
}
31-
32-
impl VirialCoeff {
33-
// pub fn from_pmf_iterator(pomf: impl Iterator<Item = (f32, f32)>) -> Self {
34-
// Self::from_pmf(&pomf.collect::<Vec<_>>())
35-
// }
36-
/// Calculate B2 from PMF data, pairs of (r, w(r)).
37-
/// w(r) should be in units of kT; r should be equidistant and in Å.
38-
/// Now calculate the osmotic second virial coefficient by integration:
39-
/// 𝐵₂ = -½ ∫ [ exp(-𝛽𝑤(𝑟) ) - 1 ] 4π𝑟² d𝑟
40-
pub fn from_pmf(pomf: &[(f32, f32)]) -> anyhow::Result<Self> {
41-
// use first two distances to calculate dr
42-
let mut iter = pomf.iter();
43-
let (r0, r1) = iter
44-
.by_ref()
45-
.map(|pair| pair.0)
46-
.take(2)
47-
.collect_tuple()
48-
.ok_or_else(|| anyhow::anyhow!("Error calculating PMF dr"))?;
49-
let dr = (r1 - r0) as f64;
50-
if dr <= 0.0 {
51-
anyhow::bail!("Negative dr in PMF");
52-
}
53-
let sigma = r0 as f64; // closest distance, "σ"
54-
let b2_hardsphere = 2.0 * PI / 3.0 * sigma.powi(3);
55-
// integrate
56-
let b2 = iter
57-
.map(|(r, w)| (*r as f64, *w as f64))
58-
.map(|(r, w)| w.neg().exp_m1() * r * r)
59-
.sum::<f64>()
60-
.mul(-2.0 * PI * dr)
61-
.add(b2_hardsphere);
62-
Ok(Self { b2, sigma })
63-
}
64-
/// Distance of closest approach, σ (Å)
65-
pub fn sigma(&self) -> f64 {
66-
self.sigma
67-
}
68-
/// Hard sphere contribution to second virial coefficient, B2hs (ų)
69-
pub fn hardsphere(&self) -> f64 {
70-
2.0 * PI / 3.0 * self.sigma.powi(3)
71-
}
72-
/// Reduced second virial coefficient, B2 / B2hs
73-
pub fn reduced(&self) -> f64 {
74-
self.b2 / self.hardsphere()
75-
}
76-
/// Association constant, 𝐾𝑑⁻¹ = -2(𝐵₂ - 𝐵₂hs)
77-
/// See "Colloidal Domain" by Evans and Wennerström, 2nd Ed, p. 408
78-
pub fn association_const(&self) -> Option<f64> {
79-
const LITER_PER_CUBIC_ANGSTROM: f64 = 1e-27;
80-
let association_const =
81-
-2.0 * (self.b2 - self.hardsphere()) * LITER_PER_CUBIC_ANGSTROM * AVOGADRO_CONSTANT;
82-
if association_const.is_sign_positive() {
83-
Some(association_const)
84-
} else {
85-
None
86-
}
87-
}
88-
/// Dissociation constant, 𝐾𝑑
89-
/// See "Colloidal Domain" by Evans and Wennerström, 2nd Ed, p. 408
90-
pub fn dissociation_const(&self) -> Option<f64> {
91-
self.association_const().map(|k| k.inv())
92-
}
93-
/// Virial coefficient, B2 in mol⋅ml/g². Molar weights in g/mol.
94-
pub fn mol_ml_per_gram2(&self, mw1: f64, mw2: f64) -> f64 {
95-
const ML_PER_ANGSTROM3: f64 = 1e-24;
96-
self.b2 * ML_PER_ANGSTROM3 / (mw1 * mw2) * AVOGADRO_CONSTANT
97-
}
98-
}
99-
1008
/// Write PMF and mean energy as a function of mass center separation to file
1019
pub fn report_pmf(
10210
samples: &[(Vector3, Sample)],
@@ -116,7 +24,7 @@ pub fn report_pmf(
11624
mean_energy_data.push((r.norm() as f32, mean_energy as f32));
11725
writeln!(
11826
pmf_file,
119-
"{:.2} {:.2} {:.2}",
27+
"{:.2} {:.4} {:.4e}",
12028
r.norm(),
12129
free_energy,
12230
mean_energy
@@ -126,7 +34,7 @@ pub fn report_pmf(
12634
}
12735
});
12836

129-
let virial = VirialCoeff::from_pmf(&pmf_data)?;
37+
let virial = VirialCoeff::from_pmf(&pmf_data, None)?;
13038

13139
info!(
13240
"Second virial coefficient, 𝐵₂ = {:.2} ų",

src/virial.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
use itertools::Itertools;
2+
use num_traits::Inv;
3+
use physical_constants::AVOGADRO_CONSTANT;
4+
use std::{
5+
f64::consts::PI,
6+
ops::{Add, Mul, Neg},
7+
};
8+
9+
/// Struct for handling osmotic second virial coefficients, B2
10+
#[derive(Debug, Clone)]
11+
pub struct VirialCoeff {
12+
/// Osmotic second virial coefficient, B2 (ų)
13+
b2: f64,
14+
/// Distance of closest approach, σ (Å)
15+
sigma: f64,
16+
}
17+
18+
impl VirialCoeff {
19+
/// Calculates B2 from PMF data, pairs of (r, w(r)).
20+
/// w(r) should be in units of kT; r should be equidistant and in Å.
21+
/// 𝐵₂ = -½ ∫ [ exp(-𝛽𝑤(𝑟) ) - 1 ] 4π𝑟² d𝑟
22+
/// If σ is not provided, it is assumed to be the first distance in the PMF.
23+
pub fn from_pmf(pomf: &[(f32, f32)], sigma: Option<f32>) -> anyhow::Result<Self> {
24+
// use first two distances to calculate dr and assume it's constant
25+
let (r0, r1) = pomf
26+
.iter()
27+
.map(|pair| pair.0)
28+
.take(2)
29+
.collect_tuple()
30+
.ok_or_else(|| anyhow::anyhow!("Error calculating PMF dr"))?;
31+
let dr = (r1 - r0) as f64;
32+
if dr <= 0.0 {
33+
anyhow::bail!("Negative dr in PMF");
34+
}
35+
let sigma = sigma.unwrap_or(r0) as f64; // closest distance, "σ"
36+
let b2_hardsphere = 2.0 * PI / 3.0 * sigma.powi(3);
37+
// integrate
38+
let b2 = pomf
39+
.iter()
40+
.map(|(r, w)| (*r as f64, *w as f64))
41+
.filter(|(r, _)| *r >= sigma)
42+
.map(|(r, w)| w.neg().exp_m1() * r * r)
43+
.sum::<f64>()
44+
.mul(-2.0 * PI * dr)
45+
.add(b2_hardsphere);
46+
Ok(Self { b2, sigma })
47+
}
48+
49+
/// Constructs a new VirialCoeff from raw parts
50+
pub fn from_raw_parts(b2: f64, sigma: f64) -> Self {
51+
Self { b2, sigma }
52+
}
53+
54+
/// Virial coefficient, B2 (ų)
55+
pub fn b2(&self) -> f64 {
56+
self.b2
57+
}
58+
/// Distance of closest approach, σ (Å)
59+
pub fn sigma(&self) -> f64 {
60+
self.sigma
61+
}
62+
/// Hard sphere contribution to second virial coefficient, B2hs (ų)
63+
pub fn hardsphere(&self) -> f64 {
64+
2.0 * PI / 3.0 * self.sigma.powi(3)
65+
}
66+
/// Reduced second virial coefficient, B2 / B2hs
67+
pub fn reduced(&self) -> f64 {
68+
self.b2 / self.hardsphere()
69+
}
70+
/// Association constant, 𝐾𝑑⁻¹ = -2(𝐵₂ - 𝐵₂hs)
71+
/// See "Colloidal Domain" by Evans and Wennerström, 2nd Ed, p. 408
72+
pub fn association_const(&self) -> Option<f64> {
73+
const LITER_PER_CUBIC_ANGSTROM: f64 = 1e-27;
74+
let association_const =
75+
-2.0 * (self.b2 - self.hardsphere()) * LITER_PER_CUBIC_ANGSTROM * AVOGADRO_CONSTANT;
76+
if association_const.is_sign_positive() {
77+
Some(association_const)
78+
} else {
79+
None
80+
}
81+
}
82+
/// Dissociation constant, 𝐾𝑑
83+
/// See "Colloidal Domain" by Evans and Wennerström, 2nd Ed, p. 408
84+
pub fn dissociation_const(&self) -> Option<f64> {
85+
self.association_const().map(|k| k.inv())
86+
}
87+
/// Virial coefficient, B2 in mol⋅ml/g². Molar weights in g/mol.
88+
pub fn mol_ml_per_gram2(&self, mw1: f64, mw2: f64) -> f64 {
89+
const ML_PER_ANGSTROM3: f64 = 1e-24;
90+
self.b2 * ML_PER_ANGSTROM3 / (mw1 * mw2) * AVOGADRO_CONSTANT
91+
}
92+
}
93+
94+
impl From<VirialCoeff> for f64 {
95+
fn from(v: VirialCoeff) -> f64 {
96+
v.b2()
97+
}
98+
}
99+
100+
#[cfg(test)]
101+
mod tests {
102+
use super::*;
103+
use approx::assert_relative_eq;
104+
#[test]
105+
fn test_virial_coeff() {
106+
let pmf = vec![
107+
(37.0, 20.0772),
108+
(38.0, 10.3099),
109+
(39.0, 4.8785),
110+
(40.0, 1.6420),
111+
(41.0, -0.2038),
112+
(42.0, -0.8156),
113+
(43.0, -0.8042),
114+
(44.0, -0.6059),
115+
(45.0, -0.3888),
116+
(46.0, -0.2398),
117+
(47.0, -0.1417),
118+
(48.0, -0.0774),
119+
(49.0, -0.0356),
120+
];
121+
let virial = VirialCoeff::from_pmf(&pmf, None).unwrap();
122+
assert_relative_eq!(virial.b2(), 87041.72562419297);
123+
assert_relative_eq!(virial.hardsphere(), 106087.39512152252);
124+
assert_relative_eq!(virial.sigma(), 37.0);
125+
assert_relative_eq!(virial.reduced(), 0.8204718904115532);
126+
assert_relative_eq!(virial.dissociation_const().unwrap(), 0.04359361238014435);
127+
128+
let virial = VirialCoeff::from_pmf(&pmf, Some(40.0)).unwrap();
129+
assert_relative_eq!(virial.b2(), 87837.30565457643);
130+
assert_relative_eq!(virial.hardsphere(), 134041.2865531645);
131+
assert_relative_eq!(virial.sigma(), 40.0);
132+
assert_relative_eq!(virial.reduced(), 0.6553003773187279);
133+
assert_relative_eq!(virial.dissociation_const().unwrap(), 0.017969653641084746);
134+
}
135+
}

0 commit comments

Comments
 (0)