From f3690ee85c3aef56a78cfff32f683260026fdf4f Mon Sep 17 00:00:00 2001
From: Ian Bell <ian.bell@nist.gov>
Date: Tue, 18 May 2021 10:25:56 -0400
Subject: [PATCH] Script for isochoric tracing of VLE in python land

---
 interface/pybind11_wrapper.cpp |   4 +-
 notebooks/isochoric.py         | 188 +++++++++++++++++++++++++++++++++
 2 files changed, 191 insertions(+), 1 deletion(-)
 create mode 100644 notebooks/isochoric.py

diff --git a/interface/pybind11_wrapper.cpp b/interface/pybind11_wrapper.cpp
index f365d8b..3897ed0 100644
--- a/interface/pybind11_wrapper.cpp
+++ b/interface/pybind11_wrapper.cpp
@@ -30,6 +30,7 @@ void add_derivatives(py::module &m, Wrapper &cls) {
     m.def("get_splus", &id::get_splus, py::arg("model"), py::arg("T"), py::arg("rho"));
 
     m.def("build_Psir_Hessian_autodiff", &id::build_Psir_Hessian_autodiff, py::arg("model"), py::arg("T"), py::arg("rho"));
+    m.def("build_Psi_Hessian_autodiff", &id::build_Psi_Hessian_autodiff, py::arg("model"), py::arg("T"), py::arg("rho"));
     m.def("build_Psir_gradient_autodiff", &id::build_Psir_gradient_autodiff, py::arg("model"), py::arg("T"), py::arg("rho"));
 
     using vd = VirialDerivatives<Model, double, Eigen::Array<double,Eigen::Dynamic,1>>;
@@ -46,6 +47,7 @@ void add_derivatives(py::module &m, Wrapper &cls) {
     //cls.def("get_Ar10", [](const Model& m, const double T, const Eigen::ArrayXd& rhovec) { return id::get_Ar10(m, T, rhovec); });
     using tdx = TDXDerivatives<Model, double, Eigen::Array<double, Eigen::Dynamic, 1> >;
     using RAX = Eigen::Ref<Eigen::ArrayXd>;
+    cls.def("get_R", [](const Model& m, const RAX molefrac) { return m.R(molefrac); }, py::arg("molefrac").noconvert());
     cls.def("get_Ar00", [](const Model& m, const double T, const double rho, const RAX molefrac) { return tdx::get_Ar00(m, T, rho, molefrac); }, py::arg("T"), py::arg("rho"), py::arg("molefrac").noconvert());
     cls.def("get_Ar01", [](const Model& m, const double T, const double rho, const RAX molefrac) { return tdx::template get_Ar01<ADBackends::autodiff>(m, T, rho, molefrac); }, py::arg("T"), py::arg("rho"), py::arg("molefrac").noconvert());
     cls.def("get_Ar10", [](const Model& m, const double T, const double rho, const RAX molefrac) { return tdx::template get_Ar10<ADBackends::autodiff>(m, T, rho, molefrac); }, py::arg("T"), py::arg("rho"), py::arg("molefrac").noconvert());
@@ -58,7 +60,7 @@ void add_derivatives(py::module &m, Wrapper &cls) {
     cls.def("get_Ar04n", [](const Model& m, const double T, const double rho, const RAX molefrac) { return tdx::template get_Ar0n<4>(m, T, rho, molefrac); }, py::arg("T"), py::arg("rho"), py::arg("molefrac").noconvert());
     cls.def("get_Ar05n", [](const Model& m, const double T, const double rho, const RAX molefrac) { return tdx::template get_Ar0n<5>(m, T, rho, molefrac); }, py::arg("T"), py::arg("rho"), py::arg("molefrac").noconvert());
     cls.def("get_Ar06n", [](const Model& m, const double T, const double rho, const RAX molefrac) { return tdx::template get_Ar0n<6>(m, T, rho, molefrac); }, py::arg("T"), py::arg("rho"), py::arg("molefrac").noconvert());
-    cls.def("get_neff", [](const Model& m, const double T, const double rho, const RAX molefrac) { return tdx::get_neff(m, T, rho, molefrac); }, py::arg("T"), py::arg("rho"), py::arg("molefrac").noconvert());
+    cls.def("get_neff",  [](const Model& m, const double T, const double rho, const RAX molefrac) { return tdx::get_neff(m, T, rho, molefrac); }, py::arg("T"), py::arg("rho"), py::arg("molefrac").noconvert());
 }
 
 /// Instantiate "instances" of models (really wrapped Python versions of the models), and then attach all derivative methods
diff --git a/notebooks/isochoric.py b/notebooks/isochoric.py
new file mode 100644
index 0000000..532b7e0
--- /dev/null
+++ b/notebooks/isochoric.py
@@ -0,0 +1,188 @@
+import timeit, sys
+
+import numpy as np
+import pandas
+from scipy.integrate import solve_ivp
+import matplotlib.pyplot as plt
+
+import VLEIsoTracer as vle
+# print('VLEIsoTracer is located at:', vle.__file__)
+IMPOSED_T = vle.VLEIsolineTracer.imposed_variable.IMPOSED_T
+
+sys.path.append('../bld/Release')
+import teqp
+import CoolProp.CoolProp as CP
+
+def get_drhovecdp_sat(model, T, rhovecL, rhovecV):
+    Hliq = teqp.build_Psi_Hessian_autodiff(model, T, rhovecL)
+    Hvap = teqp.build_Psi_Hessian_autodiff(model, T, rhovecV)
+    Hvap[~np.isfinite(Hvap)] = 1e20
+    Hliq[~np.isfinite(Hliq)] = 1e20
+
+    N = len(rhovecL)
+    A = np.zeros((N, N))
+    b = np.ones((N, 1))
+    assert(len(rhovecL)==len(rhovecV))
+    if np.all(rhovecL != 0) and np.all(rhovecV != 0):
+        A[0,0] = np.dot(Hliq[0,:], rhovecV)
+        A[0,1] = np.dot(Hliq[1,:], rhovecV)
+        A[1,0] = np.dot(Hliq[0,:], rhovecL)
+        A[1,1] = np.dot(Hliq[1,:], rhovecL)
+        
+        drhodp_liq = np.linalg.solve(A, b).squeeze()
+        drhodp_vap = np.linalg.solve(Hvap, np.dot(Hliq,drhodp_liq))
+        return drhodp_liq, drhodp_vap
+    else:
+        # Special treatment for infinite dilution
+        
+        murL = teqp.build_Psir_gradient_autodiff( model, T, rhovecL)
+        murV = teqp.build_Psir_gradient_autodiff( model, T, rhovecV)
+        RL = model.get_R(rhovecL/rhovecL.sum())
+        RV = model.get_R(rhovecV/rhovecV.sum())
+
+        # First, for the liquid part
+        for i in range(N):
+            for j in range(N):
+                if rhovecL[j] == 0:
+                    # Analysis is special if j is the index that is a zero concentration. If you are multiplying by the vector
+                    # of liquid concentrations, a different treatment than the case where you multiply by the vector
+                    # of vapor concentrations is required
+                    # ...
+                    # Initial values
+                    Aij = Hliq[j,:]*(rhovecV if i == 0 else rhovecL) # coefficient-wise product
+                    # A throwaway boolean for clarity
+                    is_liq = (i==1)
+                    # Apply correction to the j term (RT if liquid, RT*phi for vapor)
+                    Aij[j] = RL*T if is_liq else RL*T*np.exp(-(murV[j] - murL[j])/(RL*T))
+                    # Fill in entry
+                    A[i, j] = Aij.sum()
+                else:
+                    # Normal
+                    A[i, j] = np.dot(Hliq[j,:], rhovecV if (i == 0) else rhovecL)
+        drhodp_liq = np.linalg.solve(A, b)
+
+        # Then, for the vapor part, also requiring special treatment
+        # Left-multiplication of both sides of equation by diagonal matrix with liquid concentrations along diagonal, all others zero
+        diagrhovecL = np.diagflat(rhovecL)
+        PSIVstar = diagrhovecL@Hvap
+        PSILstar = diagrhovecL@Hliq
+        for j in range(N):
+            if rhovecL[j] == 0:
+                PSILstar[j, j] = RL*T
+                PSIVstar[j, j] = RV*T/np.exp(-(murV[j] - murL[j])/(RV*T))
+        drhodp_vap = np.linalg.solve(PSIVstar, PSILstar@drhodp_liq)
+
+        return drhodp_liq.squeeze(), drhodp_vap.squeeze()
+
+def get_dxdp(*, rhovec, drhovecdp):
+    rhomolar = np.sum(rhovec)
+    molefrac = rhovec/rhomolar
+    drhodp = np.sum(drhovecdp)
+    return 1.0/rhomolar*(drhovecdp-molefrac*drhodp)
+
+def get_drhovecdx(*, i, model, T, rhovec):
+    rhovecL = rhovec[0:2]
+    rhovecV = rhovec[2::]
+    drhovecdpL, drhovecdpV = get_drhovecdp_sat(model, T, rhovecL, rhovecV)
+    dpdxL = 1.0/get_dxdp(rhovec=rhovecL, drhovecdp=drhovecdpL)
+    return np.array((drhovecdpL*dpdxL[i]).tolist()+(drhovecdpV*dpdxL[i]).tolist())
+
+def traceT(model, T, rhovec0):
+    R = model.get_R(np.array([0.0, 1.0]))
+
+    # rhovec = rhovec0.copy()
+    # x = 0; dx = 1e-3
+    # o = []
+    # tic = timeit.default_timer()
+    # while x < 0.5:
+    #     pr = teqp.get_pr(model, T, rhovec[0:2])
+    #     molefrac = rhovec[0:2]/rhovec[0:2].sum()
+    #     p = pr + R*T*rhovec[0:2].sum()
+    #     f1 = get_drhovecdx(i=0, model=model, T=T, rhovec=rhovec)
+    #     ypred = rhovec + dx*f1
+    #     f2 = get_drhovecdx(i=0, model=model, T=T, rhovec=ypred)
+    #     favg = (f1+f2)/2.0
+    #     increment = dx*favg
+    #     x += dx
+    #     rhovec += increment
+    #     o.append(dict(x=x, p=p, y=rhovec[2]/rhovec[2:4].sum()))
+    # toc = timeit.default_timer()
+    # print(toc-tic, 'seconds with python+teqp tracing')
+    # df = pandas.DataFrame(o)
+    # plt.plot(df.x, df.p, 'k.', ms=1)
+    # plt.plot(df.y, df.p, 'k.', ms=1)
+
+    def rhovecprime(x, rhovec):
+        return get_drhovecdx(i=0, model=model, T=T, rhovec=rhovec)
+
+    tic = timeit.default_timer()
+    sol = solve_ivp(rhovecprime, [0.0, 1.0], y0=rhovec0, method='RK45')
+    p = [teqp.get_pr(model, T, sol.y[0:2,j]) + R*T*sol.y[0:2,j].sum() for j in range(len(sol.t))]
+    toc = timeit.default_timer()
+    print(toc-tic)
+    print(sol.nfev)
+    plt.plot(sol.t, p, 'r')
+
+    plt.ylim(0.00019e6, 0.026e6)
+    plt.gca().set(xlabel='$x_1$ / mole frac.', ylabel='$p$ / Pa')
+    plt.tight_layout(pad=0.2)
+    plt.show()
+
+def main():
+    T = 300
+    names = ['n-Hexane', 'n-Octane']
+
+    backend = 'HEOS'
+    tracer = vle.VLEIsolineTracer(IMPOSED_T, T, backend, names)
+    # Set flags
+    tracer.set_allowable_error(1e-6)
+    tracer.polishing(False)
+    tracer.set_debug_polishing(True)
+
+    tracer.set_forwards_integration(True)
+    tracer.set_unstable_termination(False)
+    tracer.set_stepping_variable(vle.VLEIsolineTracer.stepping_variable.STEP_IN_RHO1)
+
+    tracer.trace()
+    data = tracer.get_tracer_data()
+    print(tracer.get_termination_reason())
+    print(tracer.get_tracing_time())
+    
+    fig, ax4 = plt.subplots(1,1,figsize=(6, 4))
+    x = np.array(data.x).T[0]
+    x1 = np.array(data.x).T[1]
+    y = np.array(data.y).T[0]
+    pL = np.array(data.pL)
+    rhoLmat = np.array(data.rhoL)
+    rhoVmat = np.array(data.rhoV)
+    # ax4.plot(x, rhoLmat[:,0],color='b')
+    # ax4.plot(x, rhoLmat[:,1],color='r')
+    # ax4.plot(x, rhoVmat[:,0],dashes = [2,2], color='b')
+    # ax4.plot(x, rhoVmat[:,1],dashes = [2,2], color='r')
+    dx0dpL = np.diff(x)/np.diff(pL)
+    dx1dpL = np.diff(x1)/np.diff(pL)
+    ax4.plot(x, pL)
+    ax4.plot(y, pL, dashes=[2, 2])
+    
+    model = teqp.build_multifluid_model(names, '../mycp', '../mycp/dev/mixtures/mixture_binary_pairs.json')
+
+    # For now, using the EOS directly to get started... (ultimately either superancillary or extrapolation from critical)
+    AS = CP.AbstractState('HEOS', names[1])
+    AS.update(CP.QT_INPUTS, 0, T)
+    rhoL = AS.saturated_liquid_keyed_output(CP.iDmolar)
+    rhoV = AS.saturated_vapor_keyed_output(CP.iDmolar)
+    rhovec = np.array([0, rhoL, 0, rhoV])
+
+    # Initial slopes of dpdx1 and dpdy1
+    drhodp_liq, drhodp_vap = get_drhovecdp_sat(model, T, rhovec[0:2], rhovec[2::])
+    dpdxL = 1.0/get_dxdp(rhovec=rhovec[0:2], drhovecdp=drhodp_liq)
+    dpdxV = 1.0/get_dxdp(rhovec=rhovec[2::], drhovecdp=drhodp_vap)
+    xx = np.linspace(0, 0.4)
+    plt.plot(xx, xx*dpdxL[0]+AS.p(), dashes=[2, 2])
+    plt.plot(xx, xx*dpdxV[0]+AS.p(), dashes=[2, 2])
+
+    traceT(model, T, rhovec)
+
+
+if __name__ == '__main__':
+    main()
\ No newline at end of file
-- 
GitLab