Model Developer step-by-step tutorial for the creation of the Pasteurization example

Introduction

Intended audience

Model developers who want to develop process models with the LIBPF® process modeling technology.

Scope

Give a step-by-step tutorial and a reference for the steps required to re-create from scratch the Pasteurization example.

Directory structure

Each kernel / service is typically arranged in a subdirectory of the LIBPF® SDK.

For this example we assume there is a pasteur directory for the kernel / service. Inside the kernel / service subdirectory, create two directories include and src.

The resulting directory structure is then:

LIBPF --- include
       |
       L- src
       |
       L- scripts
       |
       L- pasteur --- include
       |          |
       |          L--- src
       |          |
       |          L--- ...
       |
       L- ...

Declare the classes

Multiple models can be provided by one kernel.

The best practice is to declare each class in a separate file, with the same name as the class.

In this example there is only one, the Pasteur class so create a header file in the pasteur/include directory and name it “Pasteur.h”

Type (or copy-paste) the flowsheet model declaration:


class Pasteur : public FlowSheet {
private:
  const static std::string type_;
public:
  // LIFECYCLE
  Pasteur(Libpf::User::Defaults defaults, uint32_t id=0, Persistency *persistency=nullptr, Persistent *parent=nullptr, Persistent *root=nullptr);

  // CUSTOM variables
  Quantity Qin; ///< Thermal power input
  Quantity Win; ///< Work power input
  Quantity Qout; ///< Thermal power output
  Quantity Acooler; ///< Cooler exchange surface
  Quantity Aheater; ///< Heater exchange surface
  Quantity Arecuperator; ///< Recuperator exchange surface
  Quantity coolT; ///< Cooling temperature
  Quantity highT; ///< Heating temperature
  Quantity holdTime; ///< Time to keep fluid to designed temperature
  Quantity dTexchangeLL; ///< Delta T approach for liq-liq exchanger
  Quantity feed; ///< Feedstock fluid mass flow
  Quantity press; ///< Fluid outlet relative pressure
  Quantity hs; ///< design speed in the holding pipe

  String processType; ///<  Process type selection (it selects Temperature and time for the pasteurization)
  String feedType; ///< Feed type selection - It selects the fluid composition to be pasteurization

  // CUSTOM function

  // MANDATORY
  const std::string &type(void) const { return type_; }
  void makeUserEquations(std::list<Assignment *>::iterator &p);
  void setup(void);
  void pre(SolutionMode solutionMode, int level) { }
  void post(SolutionMode solutionMode, int level);

  // NON-MANDATORY
  bool supportsSimultaneous(void);
  int maximumIterations(void);
  int sequential(void);
}; // Pasteur

We strongly suggest to start here the code documentation in a doxygen-fashion.

Take care of “CUSTOM variables”, “MANDATORY” methods and “NON-MANDATORY” methods.

The “CUSTOM variables” are input/output parameters, that the model will show in the foreground. Some of them are simply pulled-up internal variable (see FlowSheet::pre and FlowSheet::post).Other can be involved in complex calculation (see Makeuserequation).

Driver

Create a model driver file in the pasteur/src directory and name it “PasteurDriver.cc”

Include all model headers

If your models’ declarations are scattered in several header files, include them all.

In this example there is only one:


// include specific headers
#include <Pasteur.h>

Register the kernel / service

A kernel / service must be registered with:

For the pasteur example:


// register the service
Libpf::User::KernelImplementation impl_("Pasteurize", // name
  "Process modeling of continuous pasteurization processes", // description
  "(C) Copyright 2014-2015 Paolo Greppi simevo s.r.l.", // license
  "00.03.01 [2015/04/10 07:00:00]", // version
  "Pasteur", // defaultType
  "62e3a255-c1a0-4dea-a650-05b9a4a33aef" // uuid
);

Register the provided types

Here the types are registered, along with a description and a list of options that will be used by the various interfaces to present these information to the user.

Type aliases can be defined, where a base type is pre-configured with certain default values for some options.

String options are defined using enumerators (see below).


    // register the provided types
    nodeFactory.registerType<Pasteur>("Pasteur", // type name
                                      "Generic continuous pasteurization process", // type description
                                      "flowsheet", // type category
                                      true, // can be instantiated as a to-level model (Case)
                                      { }, // integer options
                                      { Libpf::User::StringOption("processType", "adjusts temperature and holding time", "HTST15", "processType"),
                                      Libpf::User::StringOption("feedType", "sets a predefined composition for the fluid to be processed", "chocolateIceCream", "feedType") }, // string options
                                      "Pasteur", 80.0, 80.0); // icon and icon size
    nodeFactory.registerAlias("Pasteur", // base type
                              "PasteurHTST15_milkWhole", // alias
                              "High Temperature Short Time Pasteurization 15 s of whole milk", // alias description
                              Libpf::User::Defaults()("processType", "HTST15")("feedType","milkWhole"), "PasteurHTST15_milkWhole", 80.0, 80.0); // icon and icon size

We suggest to keep eventual tentative models, components, and model registrations. When you do not need them anymore just comment “//” them.

Populate the list of components

These components will be available for all the models using this driver.

In this case we will use four components already available in LIBPF®:


    // populate the list of components
    components.addcomp(new purecomps::water("water"));
    components.addcomp(new purecomps::Protein("prote"));
    components.addcomp(new purecomps::Lipid("lipid"));
    components.addcomp(new purecomps::Carbohydrate("carbo"));
    components.addcomp(new purecomps::Fiber("fiber"));

Populate the enumerators

These enumerators can be used to supply the string options during type construction


    // populate enumerators
    // supported values: HTST25, HTST15, HHST3, HHST1 and user (default)
    Libpf::User::Enumerator processType("processType", "Process type selection - It selects Temperature and time for the pasteurization");
    processType.addOption(Libpf::User::Option("HTST25", "High Temperature Short Time 25 s @80 C"));
    processType.addOption(Libpf::User::Option("HTST15", "High Temperature Short Time 15 s @83 C"));
    processType.addOption(Libpf::User::Option("HHST3", "Higher Heat Shorter Time 3 s @90 C"));
    processType.addOption(Libpf::User::Option("HHST1", "Higher Heat Shorter Time 1 s @90 C"));
    processType.addOption(Libpf::User::Option("user", "(default) user specified"));
    Libpf::User::addEnumerator(processType);
    // supported values: milkWhole, chocolateIceCream, eggWhole, vanillaFatFreeIceCream and user (default)
    Libpf::User::Enumerator feedType("feedType", "Feed type selection - It selects the fluid composition to be pasteurization");
    feedType.addOption(Libpf::User::Option("milkWhole", "Milk, whole, 3.25% milkfat, with added vitamin D"));
    feedType.addOption(Libpf::User::Option("chocolateIceCream", "Ice cream, soft serve, chocolate"));
    feedType.addOption(Libpf::User::Option("eggWhole", "Egg, whole, raw, fresh"));
    feedType.addOption(Libpf::User::Option("vanillaFatFreeIceCream", "Ice creams, BREYERS, 98% Fat Free Vanilla"));
    feedType.addOption(Libpf::User::Option("user", "(default) user specified defaults to pure water"));
    Libpf::User::addEnumerator(feedType);

Define the classes

Now it is possible to start to write the actual model.

Multiple models can be provided by one kernel.

As for the class declaration in separate headers, the best practice is to define each class in a separate file, with the same name as the class.

In this example there is only one, the Pasteur class so create the model source file in the pasteur/src directory and name it “Pasteur.cc”

The usual workflow is an iterative improvement of the model through the following steps:

Implement the constructor

The class constructor must contain:

Register variables

The “CUSTOM variables” declared in the Pasteur.h file must be “registered” as part of the model.

There are two required steps:


  DEFINE(Qin, "Thermal power input", 0.0, "W"),
  DEFINE(Win, "Work power input", 0.0, "W"),
  DEFINE(Qout, "Thermal power output", 0.0, "W"),
  DEFINE(Acooler, "Cooler exchange surface", 1.0, "m^2"),
  DEFINE(Aheater, "Heater exchange surface", 1.0, "m^2"),
  DEFINE(Arecuperator, "Recuperator exchange surface", 1.0, "m^2"),
  DEFINE(coolT, "Cooling temperature", 5.0 + 273.15, "K"),
  DEFINE(highT, "Heating temperature", 90.0 + 273.15, "K"),
  DEFINE(holdTime, "Time to keep fluid to designed temperature", 0.50, "s"),
  DEFINE(dTexchangeLL, "Delta T approach for liq-liq exchanger", 20.0, "K"),
  DEFINE(feed, "Feedstock fluid mass flow", 1.0, "kg/s"),
  DEFINE(press, "Fluid outlet relative pressure", 10.0, "bar"),
  DEFINE(hs, "Design speed in the holding pipe", 1.0, "m/s"),
  // supported values: HTST25, HTST15, HHST3, HHST1 and user (default)
  DEFINE(processType, "Process type selection - It selects Temperature and time for the pasteurization", "user"),
  // supported values: milkWhole, chocolateIceCream, eggWhole, vanillaFatFreeIceCream and user (default)
  DEFINE(feedType, "Feed type selection - It selects the fluid composition to be pasteurization", "user")

  addVariable(Qin);
  addVariable(Win);
  addVariable(Qout);
  addVariable(Acooler);
  addVariable(Aheater);
  addVariable(Arecuperator);
  addVariable(coolT);
  addVariable(highT);
  addVariable(holdTime);
  addVariable(dTexchangeLL);
  addVariable(feed);
  addVariable(press);
  addVariable(hs);  
  addVariable(processType);
  addVariable(feedType);

Process options

If the model supports any string or integer option, specify in this section their default value; for each string option also specify the associated enumerator.


  // retrieve string options
  processType = retrieveString(defaults, id, persistency,
                               "processType", // enumerator
                               "user"); // default value
  feedType = retrieveString(defaults, id, persistency,
                            "feedType", // enumerator
                            "user"); // default value

Define the units

We suggest to start always with the simplest model possible (i.e. none or one unit operation) and try to connect this with source and sink vertexes.

Units are vertexes defined using strings which specify: the type of the unit, a custom name and a description.

It is also possible to specify some options. These options are useful to pass the number of stages or the number of splits or the reaction list or any other unit-specific-supported option.


    addUnit("Exchanger", defaults.relay("REC",  "Recuperator"));
    addUnit("Pump", defaults.relay("PUMP", "Liquid circulation pump"));
    addUnit("FlashDrum", defaults.relay("HEATER",  "Pasteurization heater"));
    addUnit("Pipe", defaults.relay("HOLD",  "Holding pipe"));
    addUnit("FlashDrum", defaults.relay("COOLER",  "Cooler"));

Define the streams and the flowsheet connections

Streams are edges which connect vertexes. The streams are defined using strings which specify: the type of the stream, a custom name and a description.

In addition to the user-defined units, all flowsheets come with the predefined “source” and “sink” vertexes, that represent the battery limits.

The inlet streams to the flowsheet come from the “source” vertex “out” port. The outlet streams from the flowsheet go to the “sink” vertex “in” port.


    addStream("StreamLiquid", defaults.relay("S01", "Fluid inlet"), "source", "out", "REC", "coldin");
    addStream("StreamLiquid", defaults.relay("S02", "Pre-heated fluid"), "REC", "coldout", "PUMP", "in");
    addStream("StreamLiquid", defaults.relay("S03", "Pumped pre-heated fluid"), "PUMP", "out", "HEATER", "in");
    addStream("StreamLiquid", defaults.relay("S04", "Holding hot fluid"), "HEATER", "out", "HOLD", "in");
    addStream("StreamLiquid", defaults.relay("S05", "Pasteurized fluid"), "HOLD", "out", "REC", "hotin");
    addStream("StreamLiquid", defaults.relay("S06", "Warm fluid"), "REC", "hotout", "COOLER", "in");
    addStream("StreamLiquid", defaults.relay("S07", "Cooled fluid outlet"), "COOLER", "out", "sink", "in");

The registration of vertex and edge is sequential, so before the “addStream” of a certain stream, the “addUnit” connected by that stream must be known.

Fine-tune the graphical appearance

It is also possible to customize here the icons and icon sized of the units that will be used in the process flow diagram


  my_cast<VertexBase *>(&at("HEATER"), CURRENT_FUNCTION)->setIcon("Exchanger", 100.0, 100.0);
  my_cast<VertexBase *>(&at("COOLER"), CURRENT_FUNCTION)->setIcon("Exchanger", 60.0, 60.0);
  my_cast<VertexBase *>(&at("PUMP"), CURRENT_FUNCTION)->setIcon("Pump", 40.0, 40.0); // just to resize the pump icon
  my_cast<VertexBase *>(&at("HOLD"), CURRENT_FUNCTION)->setIcon("Pipe", 100.0, 20.0); // just to stretch the pipe icon

Implement the setup

This method initializes the model by setting up input variables at their default value and by providing estimates to some results to help convergence.

Process options

The options supplied to the constructor can be used to customize the model here:


    highT.unSetInput();
    highT.setOutput();
    holdTime.unSetInput();
    holdTime.setOutput();

    // "Grade A Pasteurized Milk Ordinance" - 2011 Revision
    // https://www.fda.gov/food/hazard-analysis-critical-control-point-haccp/dairy-grade-voluntary-haccp
    // table pag. 8 (pag. 25 of the PDF)

    // High Temperature Short Time
    if (processType.value() == "HTST25") {
      highT = Value(80.0 + 273.15, "K");
      holdTime = Value(25.0, "s");
    } // HTST25
    else if (processType.value() == "HTST15") {
      highT = Value(83.0 + 273.15, "K");
      holdTime = Value(15.0, "s");
    } // HTST15
    // Higher Heat Shorter Time
    else if (processType.value() == "HHST3") {
      highT = Value(90.0 + 273.15, "K");
      holdTime = Value(3.0, "s");
    } // HHST3
    else if (processType.value() == "HHST1") {
      highT = Value(90.0 + 273.15, "K");
      holdTime = Value(1.0, "s");
    } // HHST1
    else if (processType.value() == "user") {
      // default process settings
      highT.set(90.0 + 273.15, "K");
      holdTime.set(1.0, "s");
      highT.setInput();
      holdTime.setInput();
      highT.unSetOutput();
      holdTime.unSetOutput();
    } else {
      throw ErrorRunTime("unsupported processType", CURRENT_FUNCTION);
    }

Specify inlet streams

Provide default values for the inlet streams to the flowsheet:


    // Fluid inlet
    Q("S01.T").set(4.0+273.15,"K"); // typical storage temperature for milk products and ice cream mix
    Q("S01.P").set(1.0, "atm");
    my_cast<Stream *>(&at("S01"), CURRENT_FUNCTION)->setFlash(FlashMode::PT);
    S("S01.flowoption") = "Mx";
    Q("S01:Tphase.mdot").set(feed); // assignment

    my_cast<Stream *>(&at("S01"), CURRENT_FUNCTION)->clearcomposition();
    // Feed compositions from:   
    // USDA (United States Department of Agriculture) Agricultural Research Service
    // National Nutrient Database for Standard Reference, Release 26 
    // https://data.nal.usda.gov/dataset/usda-national-nutrient-database-standard-reference-legacy-release
    if (feedType.value() == "milkWhole") {
      // reference n. 01077 "Milk, whole, 3.25% milkfat, with added vitamin D", unnormalized sum = 0.9933
      Q("S01:Tphase.x", "water") = Value(0.8813);
      Q("S01:Tphase.x", "prote") = Value(0.0315);
      Q("S01:Tphase.x", "lipid") = Value(0.0325);
      Q("S01:Tphase.x", "carbo") = Value(0.0480);
      Q("S01:Tphase.x", "fiber") = Value(0.0);
    } // milkWhole
    else if (feedType.value() == "chocolateIceCream") {
      // reference n. 01236 "Ice cream, soft serve, chocolate", unnormalized sum = 0.998
      Q("S01:Tphase.x", "water") = Value(0.598);
      Q("S01:Tphase.x", "prote") = Value(0.041);
      Q("S01:Tphase.x", "lipid") = Value(0.130);
      Q("S01:Tphase.x", "carbo") = Value(0.222);
      Q("S01:Tphase.x", "fiber") = Value(0.007);
    } // chocolateIceCream
    else if (feedType.value() == "eggWhole") {
      // reference n. 01123 "Egg, whole, raw, fresh", unnormalized sum = 1.0542
      Q("S01:Tphase.x", "water") = Value(0.7615);
      Q("S01:Tphase.x", "prote") = Value(0.1256);
      Q("S01:Tphase.x", "lipid") = Value(0.0951);
      Q("S01:Tphase.x", "carbo") = Value(0.072);
      Q("S01:Tphase.x", "fiber") = Value(0.0);
    } // eggWhole
    else if (feedType.value() == "vanillaFatFreeIceCream") {
      // reference n. 19877 "Ice creams, BREYERS, 98% Fat Free Vanilla", unnormalized sum = 1.0483
      Q("S01:Tphase.x", "water") = Value(0.6342);
      Q("S01:Tphase.x", "prote") = Value(0.0330);
      Q("S01:Tphase.x", "lipid") = Value(0.0220);
      Q("S01:Tphase.x", "carbo") = Value(0.3051);
      Q("S01:Tphase.x", "fiber") = Value(0.054);
    } // vanillaFatFreeIceCream
    else if (feedType.value() == "user") {
      // user-inputted values, defaults to pure water
      Q("S01:Tphase.x", "water").set(1.0);
      Q("S01:Tphase.x", "prote").set(0.0);
      Q("S01:Tphase.x", "lipid").set(0.0);
      Q("S01:Tphase.x", "carbo").set(0.0);
      Q("S01:Tphase.x", "fiber").set(0.0);

    } else {
      throw ErrorRunTime("unsupported feedType", CURRENT_FUNCTION);
    }

Specify unit operations

Provide default values for the operating conditions of the units:


    // Recuperator
    S("REC.hotoption") = "D";
    Q("REC.deltaPhot").set(100.0, "mbar");
    S("REC.coldoption") = "D";
    Q("REC.deltaPcold").set(100.0, "mbar");
    S("REC.option") = "Tcold";
    Q("REC.coldT").set(Q("S01.T") + dTexchangeLL); // assignment
    S("REC.coldflowoption") = "forward";
    S("REC.hotflowoption") = "backward";
    // U overall water-water (Perry's 8ed tab 11.3
    // 200 - 250 BTU / ( h ft^2 degF )
    // 1136 - 1420 J / ( s m^2 K )  (average 1278)
    Q("REC.U").set(1278.0,"W/(m2*K)");

    S("PUMP.option") = "DH";
    Q("PUMP.deltaP").set(- press - Value(1.0,"bar"));// - Q("REC.deltaPcold") - Q("HEATER.deltaP") - Q("HOLD.deltaP") - Q("REC.deltaPhot") - Q("COOLER.deltaP")); // assignment
    Q("PUMP.duty").set(0.0, "W");
    Q("PUMP.etaE").set(0.9);
    Q("PUMP.etaM").set(0.9);

    // Heater
    S("HEATER.option") = "DT";
    Q("HEATER.deltaP").set(100.0, "mbar");
    Q("HEATER.T").set(highT);  // assignment

    // Holding pipe
    // inputs for Pipe: de, s, L, h, eps
    Q("HOLD.de").set(50.0, "mm"); // initial value, overridden by the assignment
    Q("HOLD.s").set(5.0, "mm");
    Q("HOLD.L").set(10.0, "m"); // initial value, overridden by the assignment
    Q("HOLD.h").set(0.0, "m");
    Q("HOLD.eps").set(0.0457, "mm"); // tubo in acciaio commerciale o ghisa 0.0457 mm

    // Cooler
    S("COOLER.option") = "DT";
    Q("COOLER.deltaP").set(200.0, "mbar");
    Q("COOLER.T").set(coolT); // assignment

Cut streams:

When a flowsheet has a recycle, this must be temporary cut and initialized for the correct system calculation.

In the present example, the “recycle” is represented by the stream around the recuperation. We choose to cut and initialize the hot inlet stream (S05) of the exchanger.


    Q("S05.T").set(highT);
    Q("S05.P").set(Q("S01.P") + Q("REC.deltaPhot") + Q("COOLER.deltaP"));
    S("S05.flowoption") = "Mx";
    Q("S05:Tphase.mdot").set(feed);
    for (int i=0; i<NCOMPONENTS; ++i)
      Q("S05:Tphase.x", i).set(Q("S01:Tphase.x", i));

    diagnostic(3, "Defining cut streams");
    FlowSheet::addCut("S05");

Input/Output interfacing

Set/modify the Input/Output behavior of the model variable in order to show or hide them in the graphic interface as input or output parameters.

This operation is strongly suggested for the “CUSTOM variable” since their default behavior is to hide them all.

Sometime it could be useful to modify the default behavior of internal parameter such as the volumetric flow of the stream.


    Win.setOutput();
    Qin.setOutput();
    Qout.setOutput();
    Acooler.setOutput();
    Aheater.setOutput();
    Arecuperator.setOutput();

    coolT.setInput();
    feed.setInput();
    press.setInput();
    dTexchangeLL.setInput();

Implement the pre-calculations

You can optionally run some pre-calculate operations; the most frequent use is to compute the model inputs based on some custom variable.

Only put here explicit equations, which do not need a “simultaneous” resolution. If you have calculations that have to be solved iteratively, insert them in the makeUserEquation method below.

In this example model we do not have pre-calculate operations.

Implement the post-calculations

You can optionally run some post-calculate operations after the flowsheet has been solved.

You can have two type of post-calculation instructions:

Custom warning / error messages

Some checks on the results are possible as “warnings” or as “errors”.

This will alert in case of possible wrong behavior of the model

In this example model we do not have custom messages.

Assignment of the output “CUSTOM variables”

This is the right place to compute custom results.

Only put here explicit equations. If you have implicit calculations that have to be solved iteratively, insert them in the makeUserEquation method below.


    Qin   = - Q("HEATER.duty");
    Win   = - Q("PUMP.We");
    Qout  =   Q("COOLER.duty");
    // U overall water-water (Perry's 8ed tab 11.3
    // 200 - 250 BTU / ( h ft^2 degF )
    // 1136 - 1420 J / ( s m^2 K )  (average 1278)
    Value Uov(1278.0, "W/(m2*K)");
    // improbable if cooling media available
    Acooler = Q("COOLER.duty") / (Uov * dTexchangeLL);
    Aheater = -Q("HEATER.duty") / (Uov * dTexchangeLL);

    Arecuperator = Q("REC.A");

Implement the makeUserEquation method

This method is used to implement custom implicit user equation that have to be solved iteratively.

In this example we have just bound some internal parameter to some “CUSTOM variables”:


    MAKEASSIGNMENT(Q("S01:Tphase.mdot"), feed, "Feedstock fluid mass flow");
    MAKEASSIGNMENT(Q("REC.coldT"), Q("S01.T") + dTexchangeLL , "Recuperator hot side temperature" );
    MAKEASSIGNMENT(Q("PUMP.deltaP"), - press - Q("REC.deltaPcold") - Q("HEATER.deltaP") - Q("HOLD.deltaP") - Q("REC.deltaPhot") - Q("COOLER.deltaP"), "Pump outlet pressure");
    MAKEASSIGNMENT(Q("HEATER.T"), highT,  "Heater outlet temperature - process specific");
    MAKEASSIGNMENT(Q("HOLD.L"), hs * holdTime, "Holding pipe length");
    MAKEASSIGNMENT(Q("HOLD.de"), 2.0 * Q("HOLD.s") + (Q("HOLD.de") - 2.0 * Q("HOLD.s")) * sqrt(holdTime / Q("HOLD.tau")), "Holding pipe diameter");
    MAKEASSIGNMENT(Q("COOLER.T"), coolT, "Cooler outlet temperature");

Control flowsheet resolution

Finally there are some method useful to control the flowsheet calculation: iteration, simultaneous support and pre-sequential computation:


int Pasteur::maximumIterations(void) { return 100; }  // Pasteur::maximumIterations

int Pasteur::sequential(void) { return 5; }  // Pasteur::sequential

bool Pasteur::supportsSimultaneous(void) { return true; }; // Pasteur::supportsSimultaneous

Pasteurization Model references

LIBPF® references