PSCF v1.1
pscf/homogeneous/Mixture.cpp
1/*
2* PSCF - Molecule Self-Consistent Field Theory
3*
4* Copyright 2016 - 2022, The Regents of the University of Minnesota
5* Distributed under the terms of the GNU General Public License.
6*/
7
8#include "Mixture.h"
9#include <pscf/inter/Interaction.h>
10#include <pscf/math/LuSolver.h>
11#include <cmath>
12
13namespace Pscf {
14namespace Homogeneous {
15
16 using namespace Util;
17
18 /*
19 * Constructor.
20 */
23 molecules_(),
24 mu_(),
25 phi_(),
26 c_(),
27 w_(),
28 residual_(),
29 dX_(),
30 dWdC_(),
31 dWdPhi_(),
32 jacobian_(),
33 phiOld_(),
34 fHelmholtz_(0.0),
35 pressure_(0.0),
36 solverPtr_(0),
37 nMolecule_(0),
38 nMonomer_(0),
39 hasComposition_(false)
40 { setClassName("Mixture"); }
41
42 /*
43 * Destructor.
44 */
46 {}
47
48 /*
49 * Read all parameters and initialize.
50 */
51 void Mixture::readParameters(std::istream& in)
52 {
53 // Precondition
54 UTIL_ASSERT(molecules_.capacity() == 0);
55
56 read<int>(in, "nMonomer", nMonomer_);
57 c_.allocate(nMonomer_);
58 w_.allocate(nMonomer_);
59
60 read<int>(in, "nMolecule", nMolecule_);
61 molecules_.allocate(nMolecule_);
62 mu_.allocate(nMolecule_);
63 phi_.allocate(nMolecule_);
64 for (int i = 0; i < nMolecule_; ++i) {
65 readParamComposite(in, molecules_[i]);
66 }
67
68 validate();
69 }
70
71 void Mixture::setNMolecule(int nMolecule)
72 {
73 UTIL_ASSERT(molecules_.capacity() == 0);
74 nMolecule_ = nMolecule;
75 molecules_.allocate(nMolecule_);
76 mu_.allocate(nMolecule_);
77 phi_.allocate(nMolecule_);
78 }
79
80 void Mixture::setNMonomer(int nMonomer)
81 {
82 UTIL_ASSERT(nMonomer_ == 0);
83 nMonomer_ = nMonomer;
84 c_.allocate(nMonomer_);
85 w_.allocate(nMonomer_);
86 }
87
88 /*
89 * Set molecular and monomer volume fractions.
90 */
92 {
93 validate();
94 UTIL_ASSERT(phi.capacity() == nMolecule_);
95
96 // Set molecular volume fractions
97 double sum = 0;
98 for (int i = 0; i < nMolecule_ - 1; ++i) {
99 UTIL_CHECK(phi[i] >= 0.0);
100 UTIL_CHECK(phi[i] <= 1.0);
101 phi_[i] = phi[i];
102 sum += phi[i];
103 }
104 UTIL_CHECK(sum <= 1.0);
105 phi_[nMolecule_ -1] = 1.0 - sum;
106
107 computeC();
108 hasComposition_ = true;
109 }
110
111 void Mixture::computeC()
112 {
113 // Initialize monomer fractions to zero
114 int k;
115 for (k = 0; k < nMonomer_; ++k) {
116 c_[k] = 0.0;
117 }
118
119 // Compute monomer volume fractions
120 double concentration;
121 int i, j;
122 for (i = 0; i < nMolecule_; ++i) {
123 Molecule& mol = molecules_[i];
124 concentration = phi_[i]/mol.size();
125 for (j = 0; j < molecules_[i].nClump(); ++j) {
126 k = mol.clump(j).monomerId();
127 c_[k] += concentration*mol.clump(j).size();
128 }
129 }
130
131 // Check and normalize monomer volume fractions
132 double sum = 0.0;
133 for (k = 0; k < nMonomer_; ++k) {
134 UTIL_CHECK(c_[k] >= 0.0);
135 UTIL_CHECK(c_[k] <= 1.0);
136 sum += c_[k];
137 }
138 UTIL_CHECK(sum > 0.9999);
139 UTIL_CHECK(sum < 1.0001);
140 for (k = 0; k < nMonomer_; ++k) {
141 c_[k] /= sum;
142 }
143
144 }
145
146 /*
147 * Compute thermodynamic properties.
148 */
149 void Mixture::computeMu(Interaction const & interaction,
150 double xi)
151 {
152 UTIL_CHECK(interaction.nMonomer() == nMonomer_);
153 UTIL_CHECK(hasComposition_);
154
155 // Compute monomer excess chemical potentials
156 interaction.computeW(c_, w_);
157
158 int m; // molecule index
159 int c; // clump index
160 int t; // monomer type index
161 double mu, size;
162 for (m = 0; m < nMolecule_; ++m) {
163 Molecule& mol = molecules_[m];
164 mu = log( phi_[m] );
165 mu += xi*mol.size();
166 for (c = 0; c < mol.nClump(); ++c) {
167 t = mol.clump(c).monomerId();
168 size = mol.clump(c).size();
169 mu += size*w_[t];
170 }
171 mu_[m] = mu;
172 }
173 }
174
175 /*
176 * Compute composition from chemical potentials.
177 */
178 void Mixture::computePhi(Interaction const & interaction,
179 DArray<double> const & mu,
180 DArray<double> const & phi,
181 double& xi)
182 {
183 UTIL_ASSERT(interaction.nMonomer() == nMonomer_);
184
185 // Allocate residual and jacobian on first use.
186 if (residual_.capacity() == 0) {
187 residual_.allocate(nMonomer_);
188 dX_.allocate(nMolecule_);
189 dWdC_.allocate(nMonomer_, nMonomer_);
190 dWdPhi_.allocate(nMonomer_, nMolecule_);
191 jacobian_.allocate(nMolecule_, nMolecule_);
192 phiOld_.allocate(nMolecule_);
193 solverPtr_ = new LuSolver();
194 solverPtr_->allocate(nMolecule_);
195 }
196
197 // Compute initial state
199 computeMu(interaction, xi);
200 adjustXi(mu, xi);
201
202 // Compute initial residual
203 double error;
204 double epsilon = 1.0E-10;
205 computeResidual(mu, error);
206
207 #if 0
208 std::cout << "mu[0] =" << mu[0] << std::endl;
209 std::cout << "mu[1] =" << mu[1] << std::endl;
210 std::cout << "mu_[0] =" << mu_[0] << std::endl;
211 std::cout << "mu_[1] =" << mu_[1] << std::endl;
212 std::cout << "residual[0] =" << residual_[0] << std::endl;
213 std::cout << "residual[1] =" << residual_[1] << std::endl;
214 std::cout << "error =" << error << std::endl;
215 #endif
216
217 if (error < epsilon) return;
218
219 double s1; // clump size
220 double v1; // molecule size
221 double f1; // clump fraction
222 int m1, m2; // molecule type indices
223 int c1; // clump index
224 int t1, t2; // monomer type indices
225
226 for (int it = 0; it < 50; ++it) {
227
228 // Compute matrix of derivative dWdC (C = monomer fraction)
229 interaction.computeDwDc(c_, dWdC_);
230
231 // Compute matrix derivative dWdPhi (Phi = molecule fraction)
232 for (t1 = 0; t1 < nMonomer_; ++t1) {
233 for (m1 = 0; m1 < nMolecule_; ++m1) {
234 dWdPhi_(t1, m1) = 0.0;
235 }
236 }
237 for (m1 = 0; m1 < nMolecule_; ++m1) {
238 v1 = molecule(m1).size();
239 for (c1 = 0; c1 < molecule(m1).nClump(); ++c1) {
240 t1 = molecule(m1).clump(c1).monomerId();
241 s1 = molecule(m1).clump(c1).size();
242 f1 = s1/v1;
243 for (t2 = 0; t2 < nMonomer_; ++t2) {
244 dWdPhi_(t2, m1) += dWdC_(t2, t1)*f1;
245 }
246 }
247 }
248
249 // Compute matrix d(mu)/d(Phi), stored in jacobian_
250 for (m1 = 0; m1 < nMolecule_; ++m1) {
251 for (m2 = 0; m2 < nMolecule_; ++m2) {
252 jacobian_(m1, m2) = 0.0;
253 }
254 jacobian_(m1, m1) = 1.0/phi_[m1];
255 }
256 for (m1 = 0; m1 < nMolecule_; ++m1) {
257 v1 = molecule(m1).size();
258 for (c1 = 0; c1 < molecule(m1).nClump(); ++c1) {
259 t1 = molecule(m1).clump(c1).monomerId();
260 s1 = molecule(m1).clump(c1).size();
261 for (m2 = 0; m2 < nMolecule_; ++m2) {
262 jacobian_(m1, m2) += s1*dWdPhi_(t1, m2);
263 }
264 }
265 }
266
267 // Impose incompressibility
268 int mLast = nMolecule_ - 1;
269 for (m1 = 0; m1 < nMolecule_; ++m1) {
270 for (m2 = 0; m2 < nMolecule_ - 1; ++m2) {
271 jacobian_(m1, m2) -= jacobian_(m1, mLast);
272 }
273 // Derivative of mu_[m1] with respect to xi
274 jacobian_(m1, mLast) = molecule(m1).size();
275 }
276
277 // Newton Raphson update of phi and xi fields
278 solverPtr_->computeLU(jacobian_);
279 solverPtr_->solve(residual_, dX_);
280
281 // Store old value of phi
282 for (m1 = 0; m1 < nMolecule_; ++m1) {
283 phiOld_[m1] = phi_[m1];
284 }
285
286 // Apply update
287 bool inRange = false;
288 for (int j = 0; j < 5; ++j) {
289
290 // Update volume fractions
291 double sum = 0.0;
292 for (m1 = 0; m1 < nMolecule_ - 1; ++m1) {
293 phi_[m1] = phiOld_[m1] - dX_[m1];
294 sum += phi_[m1];
295 }
296 phi_[mLast] = 1.0 - sum;
297
298 // Check if all volume fractions are in [0,1]
299 inRange = true;
300 for (m1 = 0; m1 < nMolecule_; ++m1) {
301 if (phi_[m1] < 0.0) inRange = false;
302 if (phi_[m1] > 1.0) inRange = false;
303 }
304
305 // Exit loop or reduce increment
306 if (inRange) {
307 break;
308 } else {
309 for (m1 = 0; m1 < nMolecule_; ++m1) {
310 dX_[m1] *= 0.5;
311 }
312 }
313
314 }
315 if (inRange) {
316 xi = xi - dX_[mLast];
317 } else {
318 UTIL_THROW("Volume fractions remain out of range");
319 }
320
321 // Compute residual
322 computeC();
323 computeMu(interaction, xi);
324 adjustXi(mu, xi);
325 computeResidual(mu, error);
326
327 #if 0
328 std::cout << "mu[0] =" << mu[0] << std::endl;
329 std::cout << "mu[1] =" << mu[1] << std::endl;
330 std::cout << "mu_[0] =" << mu_[0] << std::endl;
331 std::cout << "mu_[1] =" << mu_[1] << std::endl;
332 std::cout << "residual[0] =" << residual_[0] << std::endl;
333 std::cout << "residual[1] =" << residual_[1] << std::endl;
334 std::cout << "error =" << error << std::endl;
335 #endif
336
337 if (error < epsilon) return;
338 }
339
340 UTIL_THROW("Failed to converge");
341 }
342
343 void Mixture::adjustXi(DArray<double> const & mu, double& xi)
344 {
345 double dxi = 0.0;
346 double sum = 0.0;
347 for (int i=0; i < nMolecule_; ++i) {
348 dxi += mu[i] - mu_[i];
349 sum += molecules_[i].size();
350 }
351 dxi = dxi/sum;
352 for (int i=0; i < nMolecule_; ++i) {
353 mu_[i] += dxi*molecules_[i].size();
354 }
355 xi += dxi;
356 }
357
358 void Mixture::computeResidual(DArray<double> const & mu, double& error)
359 {
360 error = 0.0;
361 for (int i = 0; i < nMonomer_; ++i) {
362 residual_[i] = mu_[i] - mu[i];
363 if (std::abs(residual_[i]) > error) {
364 error = std::abs(residual_[i]);
365 }
366 }
367 }
368
369 /*
370 * Compute Helmoltz free energy and pressure
371 */
372 void Mixture::computeFreeEnergy(Interaction const & interaction)
373 {
374 fHelmholtz_ = 0.0;
375
376 // Compute ideal gas contributions to fHelhmoltz_
377 double size;
378 for (int i = 0; i < nMolecule_; ++i) {
379 size = molecules_[i].size();
380 fHelmholtz_ += phi_[i]*( log(phi_[i]) - 1.0 )/size;
381 }
382
383 // Add average interaction free energy density per monomer
384 fHelmholtz_ += interaction.fHelmholtz(c_);
385
386 // Compute pressure
387 pressure_ = -fHelmholtz_;
388 for (int i = 0; i < nMolecule_; ++i) {
389 size = molecules_[i].size();
390 pressure_ += phi_[i]*mu_[i]/size;
391 }
392
393 }
394
395 /*
396 * Check validity after completing initialization.
397 */
398 void Mixture::validate() const
399 {
400 UTIL_ASSERT(nMolecule_ > 0);
401 UTIL_ASSERT(nMonomer_ > 0);
402 for (int i = 0; i < nMolecule_; ++i) {
403 Molecule const & mol = molecules_[i];
404 UTIL_ASSERT(mol.nClump() > 0);
405 for (int j = 0; j < mol.nClump(); ++j) {
406 UTIL_ASSERT(mol.clump(j).monomerId() < nMonomer_);
407 }
408 }
409 }
410
411} // namespace Homogeneous
412} // namespace Pscf
int monomerId() const
Get the monomer type id.
Definition: Clump.h:127
double size() const
Get the size (number of monomers) in this block.
Definition: Clump.h:133
double c(int id) const
Return monomer volume fraction for one monomer type.
void validate() const
Validate all data structures.
Molecule & molecule(int id)
Get a molecule object (non-const reference).
virtual void readParameters(std::istream &in)
Read parameters from file and initialize.
double phi(int id) const
Return molecular volume fraction for one species.
int nMonomer() const
Get number of monomer types.
void setComposition(DArray< double > const &phi)
Set system composition.
void computePhi(Interaction const &interaction, DArray< double > const &mu, DArray< double > const &phi, double &xi)
Compute composition from chemical potentials.
void computeFreeEnergy(Interaction const &interaction)
Compute Helmholtz free energy and pressure.
int nMolecule() const
Get number of molecule species (polymer + solvent).
double mu(int id) const
Return chemical potential for one species.
void setNMonomer(int nMonomer)
Set the number of monomer types.
void computeMu(Interaction const &interaction, double xi=0.0)
Compute chemical potential from preset composition.
void setNMolecule(int nMolecule)
Set the number of molecular species and allocate memory.
Descriptor of a molecular species in a homogeneous mixture.
Definition: Molecule.h:39
int nClump() const
Number of monomer clumps (monomer types).
Definition: Molecule.h:126
Clump & clump(int id)
Get a specified Clump.
Definition: Molecule.h:141
double size() const
Total molecule size = volume / reference volume.
Definition: Molecule.h:132
Flory-Huggins excess free energy model.
Definition: Interaction.h:26
virtual double fHelmholtz(Array< double > const &c) const
Compute excess Helmholtz free energy per monomer.
Definition: Interaction.cpp:92
virtual void computeDwDc(Array< double > const &c, Matrix< double > &dWdC) const
Compute second derivatives of free energy.
virtual void computeW(Array< double > const &c, Array< double > &w) const
Compute chemical potential from concentration.
int nMonomer() const
Get number of monomer types.
Definition: Interaction.h:160
Solve Ax=b by LU decomposition of A.
Definition: LuSolver.h:31
void allocate(int n)
Allocate memory.
Definition: LuSolver.cpp:46
void solve(Array< double > &b, Array< double > &x)
Solve Ax = b for known b to compute x.
Definition: LuSolver.cpp:83
void computeLU(const Matrix< double > &A)
Compute the LU decomposition for later use.
Definition: LuSolver.cpp:63
int capacity() const
Return allocated size.
Definition: Array.h:159
Dynamically allocatable contiguous array template.
Definition: DArray.h:32
void allocate(int capacity)
Allocate the underlying C array.
Definition: DArray.h:199
void allocate(int capacity1, int capacity2)
Allocate memory for a matrix.
Definition: DMatrix.h:170
An object that can read multiple parameters from file.
void setClassName(const char *className)
Set class name string.
void readParamComposite(std::istream &in, ParamComposite &child, bool next=true)
Add and read a required child ParamComposite.
#define UTIL_CHECK(condition)
Assertion macro suitable for serial or parallel production code.
Definition: global.h:68
#define UTIL_THROW(msg)
Macro for throwing an Exception, reporting function, file and line number.
Definition: global.h:51
#define UTIL_ASSERT(condition)
Assertion macro suitable for debugging serial or parallel code.
Definition: global.h:75
C++ namespace for polymer self-consistent field theory (PSCF).
Utility classes for scientific computation.
Definition: accumulators.mod:1