HOWTO calculate a liquid-liquid separation

An interesting thing that you can do with a two-phase stream such as StreamNrtl1LiquidLiquid (based on the Non Random Two Liquids activity coefficient model in the original formulation with temperature-independent bij, with two liquids) is the calculation of the liquid-liquid separation.

In this tutorial, we’ll calculate the liquid-liquid split of water and 2-ethyl-1-hexanol at 125°C.

Start from the Hello world tutorial, and perform these steps:

  1. in the #include section, include the headers required for components and streams:

     #include <libpf/purecomps.h>
     #include <libpf/persistency/NodeFactory.h>
     #include <libpf/phases/phases.h>
     #include <libpf/ListComponents.h> // for components and NCOMPONENTS
     #include <libpf/streams/Stream.h>
     #include <libpf/streams/streams.h> // register all stream types with the model factory
    
  2. since this tutorial is going to use NRTL-type streams, it is required to include the “activity” headers:

     #include <libpf/phases/ActivityNRTL.h>
     #include <libpf/phases/PhaseActivity.h>
    
  3. the liquid-liquid equilibrium requires the initialization of certain flash parameters, thus include:

     #include <libpf/streams/FlashLlTx.h>
    
  4. in the main function, remove the “Hello, world !" statement and replace it as follows:

    • define water and 2-ethyl-1-hexanol as components:

        components.addcomp(new purecomps::water);
        components.addcomp(new purecomps::Ethylhexanol("ETEX"));
      
    • NRTL1 activity phases require to set the NRTL parameters as follow (these are based on experimental data at 120.384 - 129.785°C as described here):

        Activity::Nrtl1::parameters.resize();
        Activity::Nrtl1::parameters.setB(0, 1, 3060.4764877456, -156.0296483827);
        Activity::Nrtl1::parameters.setA(0, 1, 0.2);
      
    • as above mentioned, liquid-liquid equilibrium requires a smart initialization. A practical initialization strategy is to set the KLLs for the key organic components to a small value such as 1E-5 and the KLL for for water to a large value such as 1E5; this results in the first phase (the “Vapor”) to become the watery phase, and the second phase (the “Liquid”) to become the organic phase. If the inverse KLLs values are introduced, the phase assigments will be reverted.

        FlashLlTx::resetKllEstimates();
        FlashLlTx::setKllEstimate(0, 1E5);
        FlashLlTx::setKllEstimate(1, 1E-5);
      
    • add the actual stream manipulation:

        NodeFactory nodeFactory;
        Stream *stream = my_cast<Stream *>(nodeFactory.create("StreamNrtl1LiquidLiquid", Libpf::User::Defaults("STREAM")), CURRENT_FUNCTION);
        stream->flashoption = "PT";
        stream->flowoption = "Nx";
      
        stream->Q("Tphase.ndot").set(100.0,"kmol/s");
        stream->clearcomposition();
        stream->Tphase->Q("x[0]") = 0.5; // water
        stream->Tphase->Q("x[1]") = One - stream->Tphase->Q("x[0]");  // ETEX
        stream->P.set(1.0, "atm");
        stream->T.set(125.0+273.15, "K");
      
        stream->resetErrors();
        stream->calculate();
        stream->reportMessages();
      
    • add your desired output (here the std::cout has been used so include the <iostream> header) :

        std::cout << "Calculation errors: " << stream->errors.size() << std::endl;
        std::cout << "------------------------------------------------------------------------------------" << std::endl;
        std::cout << "Phase\tfraction\tWater\tETEX\tWater\tETEX" << std::endl;
        std::cout << "Name\tmol frac\tmol frac\tndot, kmol/s\tndot, kmol/s" << std::endl;
        std::cout << stream->at("Vphase").fullTag() << "\t"
                  << stream->Q("Vphase.fraction") << "\t"
                  << stream->Q("Vphase.x[0]") << "\t"
                  << stream->Q("Vphase.x[1]") << "\t"
                  << stream->Q("Vphase.ndotcomps[0]").toDouble() << "\t"
                  << stream->Q("Vphase.ndotcomps[1]").toDouble() << std::endl;
        std::cout << stream->at("Lphase").fullTag() << "\t"
                  << stream->Q("Lphase.fraction") << "\t"
                  << stream->Q("Lphase.x[0]") << "\t"
                  << stream->Q("Lphase.x[1]") << "\t"
                  << stream->Q("Lphase.ndotcomps[0]").toDouble() << "\t"
                  << stream->Q("Lphase.ndotcomps[1]").toDouble() << std::endl;
        std::cout << stream->at("Tphase").fullTag() << "\t"
                  << stream->Q("Tphase.fraction") << "\t"
                  << stream->Q("Tphase.x[0]") << "\t"
                  << stream->Q("Tphase.x[1]") << "\t"
                  << stream->Q("Tphase.ndotcomps[0]").toDouble() << "\t"
                  << stream->Q("Tphase.ndotcomps[1]").toDouble() << std::endl;
        std::cout << "------------------------------------------------------------------------------------" << std::endl;
      
        std::cout << "Water Kll: " << stream->Q("Vphase.x[0]") / stream->Q("Lphase.x[0]") << std::endl;
        std::cout << "ETEX Kll:  " << stream->Q("Vphase.x[1]") / stream->Q("Lphase.x[1]") << std::endl;
      
    • it is possible to add some error checking such as mass conservation:

        for (int i=0;i<NCOMPONENTS;++i){
            if (fabs(stream->Q("Tphase.ndotcomps[0]")-stream->Q("Vphase.ndotcomps[0]")-stream->Q("Lphase.ndotcomps[0]")) > Value(1.0E-4,"mol/s") )
            throw ErrorRunTime(CURRENT_FUNCTION, "Unbalanced phases");
        }
      
    • at the end, remember to delete the pointer !

        delete stream;
      

This is the expected output:

* ****************** LIBPF 01.00.2350 [2016/04/12 11:39:44] ******************
* (C) Copyright 2004-2016 Paolo Greppi simevo s.r.l.
* ****************************************************************************
* All rights reserved; do not distribute without permission.
* ****************************************************************************

Calculation errors: 0
------------------------------------------------------------------------------------
Phase   fraction        Water   ETEX    Water   ETEX
Name    mol frac        mol frac        ndot, kmol/s    ndot, kmol/s
STREAM:Vphase   0.278635989043  0.999484893116  0.000515106883773       27.8492 0.0143527
STREAM:Lphase   0.721364010957  0.30706763201   0.69293236799   22.1508 49.9856
STREAM:Tphase   1       0.5     0.5     50      50
------------------------------------------------------------------------------------
Water Kll: 3.25493405663 
ETEX Kll:  0.000743372524604

How to get on

  1. Read the blog post on the effect of initial estimates for KLLs on the convergence of liquid-liquid equilibrium calculations

  2. check the streams chapter in the LIBPF® SDK Classes overview

  3. Proceed to the tutorial for the Pasteurization example