LIBPF® SDK Classes overview

Introduction

Intended audience

This document addresses those who wish to develop models with LIBPF® (model developers).

Scope

This document is the classes overview for LIBPF® (LIBrary for Process Flowsheeting) Software Development Kit (SDK) version 1.1;

Prerequisites

Low-level objects

Components

Each chemical species in LIBPF® is an instance of a Compoment class that defines it.

Component classes are all derived from the abstract ComponentGeneric class:

component hiererarchy

The fundamental scalar pure component parameters required to define a component are defined by the ComponentBase class:

Furthermore the way the properties are computed is determined by which mixins are assembled to compose a Component; mixins must be supplied for:

The most common components are defined in the header file components.h.

For example, to add water to the list of components simply use this statement:

components.addcomp(new purecomps::water);

Note: in this way the component will be called with its default name “H2O”; if you prefer to access the component with another name you can pass to the constructor in order to overwrite the default value H2O:

components.addcomp(new purecomps::water("Wasser"));

Note: The component list must be populated before the instantiation of any object that contains vector variables which are indexed by the component names. In particular HtuNtu, MultiReactionTransfer, PhaseInterface and PhaseInterfaceSimple (thus all phase types, and thus all stream types), Reaction (thus all reaction types) and Separator.

Models

Anything that can be calculated in LIBPF® must be an instance of a class derived from Model: Reactions, Phases, Streams, unit operations and flowsheets are all derived from Model.

While model developers normally subclass FlowSheet to define their own models, it is perfectly fine to subclass Model directly for simple models.

Here is an example toy class that defines and calculates the square:

class Square : public Model {
private:
  static const std::string type_;
public:
  Square(Libpf::User::Defaults defaults, uint32_t id=0, Persistency *persistency=nullptr, Persistent *parent=nullptr, Persistent *root=nullptr);
  // variables:
  Quantity A; ///< Area
  Quantity p; ///< Perimeter
  Quantity l; ///< side length
  Quantity d; ///< diagonal
  // methods:
  Category category() const { return Category::option; }
  void initializeNonPersistents(void) { }
  ObjectiveNleAd *objectiveFunction(void) { return NULL; }
  const std::string &type(void) const { return type_; }
  void setup(void);
  void calculate(int level=0);
}; // Square

Note the dummy implementations for the category, initializeNonPersistents and objectiveFunction methods. You can override printSvg as well if you would like your model to appear with a specific image as “PFD” in the user interface.

For the implementation you need to instantiate the static variabile type_:

const std::string Square::type_("Square");

and implement the three remaining methods, the constructor:

Square::Square(Libpf::User::Defaults defaults, uint32_t id, Persistency *persistency, Persistent *parent, Persistent *root) :
  Model(defaults, id, persistency, parent, root),
  DEFINE(A, "area", 0.0, "m2"),
  DEFINE(p, "perimeter", 0.0, "m") {
  DEFINE(l, "side length", 0.0, "m"),
  DEFINE(d, "diagonal", 0.0, "m") {
  addVariable(A);
  addVariable(p);
  addVariable(l);
  addVariable(d);
} // Square::Square

the setup:

void Square::setup(void) {
  l.setInput();
  A.setOutput();
  p.setOutput();
  d.setOutput();
} // Square::setup

and the calculate method:

void Square::calculate(int level) {
  resetErrors();
  A = l * l;
  p = 4.0 * l;
  d = l * sqrt(2.0);
} // Square::calculate

One important caveat is that you have to invoke the resetErrors from within calculate, else the errors / warnings from the previous runs are sticky.

Reactions

Each reaction in LIBPF® is an instance of a Reaction class that defines it.

Model developers often subclass Reaction* to define their own reactions.

Fixed-conversion stoichiometric reactions are typically defined in a ReactionYield-derived class:

class ReactionReformingCH4 : virtual public ReactionYield {
public:
  const static std::string type_;
  ReactionReformingCH4(Libpf::User::Defaults defaults, uint32_t id=0, Persistency *persistency=nullptr, Persistent *parent=nullptr, Persistent *root=nullptr);
  ~ReactionReformingCH4() { }
  void setup(void) { }
  const std::string &type(void) const {
    return type_;
  }
}; // class ReactionReformingCH4

The implementation requires that static variabile type_ is instantiated:

const std::string ReactionReformingCH4::type_("ReactionReformingCH4");

and that the constructor is implemented; no need to implement setup and calculate as in Model-derived classes because those are provided by ReactionYield.

In the constructor you define the stoichiometric coefficients and the key component (the one in terms of which the conversions are defined):

ReactionReformingCH4::ReactionReformingCH4(Libpf::User::Defaults defaults, uint32_t id, Persistency *persistency, Persistent *parent, Persistent *root) :
  ReactionYield(defaults, id, persistency, parent, root) {
  static const int verbosityLocal = 0;
  diagnostic(2, "Entered");
  I("keycomp") = components.lookup("CH4");
  setcoeff( "CH4", -1.0);
  setcoeff( "H2O", -1.0);
  setcoeff( "CO", 1.0);
  setcoeff( "H2", 3.0);
} // ReactionReformingCH4::ReactionReformingCH4

Equilibrium reactions are derived from the fixed-conversion stoichiometric reaction, and pulling in the ReactionEquilibrium mixin.

class ReactionReformingEquilibriumCH4 : public ReactionReformingCH4, public ReactionEquilibrium {
public:
  const static std::string type_;
  ReactionReformingEquilibriumCH4(const std::string &t, const std::string &d);
  ReactionReformingEquilibriumCH4(Libpf::User::Defaults defaults, uint32_t id=0, Persistency *persistency=nullptr, Persistent *parent=nullptr, Persistent *root=nullptr);
  ~ReactionReformingEquilibriumCH4() { }
  void setup(void) { }
  const std::string &type(void) const {
    return type_;
  }
}; // class ReactionReformingEquilibriumCH4

In this case the supplied conversion is used as an initial value, the actual conversion is computed so that the equilibium constant from thermodynamics (K soll-wert) matches that from the partial pressures (K ist-wert).

The implementation requires that the static variabile type_ is instantiated and a trivial constructor be implemented.

const std::string ReactionReformingEquilibriumCH4::type_("ReactionReformingEquilibriumCH4");

ReactionReformingEquilibriumCH4::ReactionReformingEquilibriumCH4(Libpf::User::Defaults defaults, uint32_t id, Persistency *persistency, Persistent *parent, Persistent *root) :
  ReactionYield(defaults, id, persistency, parent, root),
  ReactionReformingCH4(defaults, id, persistency, parent, root),
  ReactionEquilibrium(defaults, id, persistency, parent, root) { }

The equilibrium constant is automatically calculated from the Gibbs free energy data, or a user correlation can be supplied by overriding the ReactionEquilibrium::calculateKSollwert function.

In case of simultaneous equilibium reactions, identify the necessary and fundamental reactions for the complete description of the reaction system using the rank of the Brinckley matrix (reactions vs chemical species).

Phases

The phases are low-level objects used to keep track of material and energy balances and to store the properties needed to compute the streams and unit operations such as density, enthalpy, entropy, free energy, activity and fugacity.

Model developers never subclass Phase* because the built-in Phase* classes are sufficient for all purposes:

phase hierarchy

Significative phase classes are:

A PhaseIdeal object contains the following values:

// from PhaseM mixin:
Quantity fraction;  ///< phase fraction
Quantity mdot;  ///< Mass flow
std::vector<Quantity> w;         ///< Mass fraction
std::vector<Quantity> mdotcomps; ///< Mass flow of each component

// from PhaseMN mixin:
Quantity AMW;   ///< Average molecular weight
Quantity ndot;  ///< Mole flow
std::vector<Quantity> x;         ///< Mole fraction
std::vector<Quantity> ndotcomps; ///< Mole flow of each component

// from PhaseMNProperties mixin:
Quantity vdot;  ///< Volume flow
Quantity v;     ///< Molar volume
Quantity rho;   ///< Mass Density
Quantity h;     ///< Specific Enthalpy, by mass
Quantity H;     ///< Specific Enthalpy, by moles
Quantity cp;    ///< Specific Heat, by mass
Quantity Cp;    ///< Specific Heat, by moles
Quantity s;     ///< Specific Entropy, by mass
Quantity S;     ///< Specific Entropy, by moles
Quantity g;     ///< Specific Gibbs free energy, by mass

See the HOWTO manipulate Phases tutorial and the HOWTO use activity-based Phases tutorial.

Streams

The streams are intermediate-level objects used to represent material streams and calculate phase equilibria. They are implemented as the classes derived from Stream.

Model developers never subclass Stream*, but the supermodel developer may do so in some cases.

Every Stream-derived class has a pointer Tphase, which points to a PhaseTotal or PhaseSimpleTotal object. Tphase represent the total phase (the only phase or the sum of all phases).

Additionally, if the stream is multi-phase, there can be:

For two-phase streams with one inert solid phase and a vapor or liquid phase (StreamIdealSolidVapor or StreamIdealLiquidSolid) the fluid phase pointer is named Xphase.

Streams can be flashed; the flash modes are represented by the FlashMode SmartEnum:

Abbreviation Description Degrees of freedom
PT Pressure-Temperature computation P, T
PA Pressure-phase fraction computation P, Vphase->fraction
PH Pressure-Enthalpy computation P, Tphase->H (Specific Enthalpy, by moles)
RH Density-enthalpy computation Tphase->rho (Mass Density), Tphase->H (Specific Enthalpy, by moles)
RT Density-temperature computation Tphase->rho (Mass Density), T
PS Pressure-Entropy computation P, Tphase->S (Specific Entropy, by moles)
SA Entropy-phase fraction computation Tphase->S (Specific Entropy, by moles), Vphase->fraction

LIBPF® supports the phase equilibrium between 2 or 3 fluid phases (vapor-liquid, liquid-liquid, vapor-liquid-liquid) also in the presence of inert solids, using:

See the HOWTO manipulate Streams tutorial.

Even if some streams are instantiable in a static way,

 StreamTabularLiquidVapor VL(Libpf::User::Defaults("VL"));

it is strongly recomended to use the object factory way:

 NodeFactory nodeFactory;
 Stream *feed = my_cast<Stream *>(nodeFactory.create("StreamVapor", Libpf::User::Defaults("test")), CURRENT_FUNCTION);

To see the stream insertion in a flowsheet model see the flow sheet constructor description

Single phase streams

If a model requires a simple single-phase stream just for mass balances, the following stream type are available:

For a better integration in a flowsheet model, StreamOne-based types are suggested since they involve phase properties calculation:

Two-phase streams

If a model requires a simple two-phase stream just for mass balances, the following stream types are available:

For a better integration in a flowsheet model there are two main groups of two-phase streams: those which do actually calculate the equilibrium and those which do not.

The former are StreamTwo-based types. The phase equilibrium is calculated by the Flash. There are four two-phase flash types available, thus four group of two-phase streams exists:

The streams that do not calculate equilibrium involve one fluid phase and one solid phase (StreamOneSolid):

Three-phase streams

If a model requires a simple three-phase stream just for mass balances, the following stream types are available:

If phase equilibrium between the fluid phase is required, you need one of the StreamTwoSolid-based types:

Unit Operations

LIBPF® has a wide choice of pre-defined unit operation models, and the supermodel developer can define new ones.

Here you will find an overview of the most commonly used unit operation classes.

Class name Description
Compressor isentropic gas compressor or expander with an unlimited number of feeds; the inlet pressure is the minimum between the pressures of all feeds
Decanter simple decanter model with a fixed separation (does not calculate predictive triphasic separation), with one feed
Divider divides the only feed, without changing the composition and honoring mass and energy balances
Exchanger optionally reactive heat exchanger with two feeds (hot and cold) and two outlets
HtuNtu gas-liquid separation model based on the theory of the Height Transfer Unit / Number of Transfer Units, suitable for treating/non-reactive absorption or desorptions in dilute solution for environmental applications
LiquidRingVacuumPump Liquid ring vacuum pump flowsheet
Mixer adiabatic mixing an unlimited number of feeds; the inlet pressure is the minimum between the pressures of all feeds
MultiCompressorIntercooled multistage isentropic gas compressor with interrefrigeration and phase separation between the stages
MultiExchanger optionally reactive multistream heat exchanger with N feeds (hot and cold) and N outlets
Multiplier copies the only inlet stream into as many outlet streams are connected, without changing the composition, thermodynamic state and flow; it does not honor mass and energy balances - in fact it generates mass and energy out of nowhere
Splitter separates the only feed into its constituent stages in isotherm and isobar conditions

FlashDrum

FlashDrum is the model to represent an optionally reactive flash with an unlimited number of feeds and one outlet (i.e. without separation of the phases).

The type of flash (i.e. the phases present and the methods used for the calculation of fugacity) is determined by the type of the connected current as an output: if the output is StreamVapor type, only the vapour phase is present and no phase equilibrium is considered; if the output type is StreamEosLiquidSolidVapor, then the stable phases are liquid, inert solid and vapor and the vapor / liquid phase equilibrium is calculated with an equation of State.

FlashDrum supports various combinations of set variables, described compactly as a two-letter option code with structure:

{D, P, K} {T, H, S, A}

i.e. the first letter is the choice between {D, P, K} and the second of which between {T, H, S, A}.

For example, if the first letter is P, then the pressure (P) to be fixed, resulting in flash:

If the first letter is D then the pressure drop (deltaP) is fixed, and if it is K it is the discharge coefficient (flowcoefficient variable), from which the program can calculate the pressure drop with the expression:

deltaP = flowcoefficient * vdot^2

See the HOWTO manipulate Units tutorial.

FlashSplitter

FlashSplitter is an optionally reactive flash; it calculates with an unlimited number of feeds and separation of the multiphase outlet; FlashSplitter is a template class parameterized with a stream time, which sets the the type of flash (i.e. the phases present and the methods used for the calculation of fugacity).

If it is instantiated as FlashSplitter<StreamEosLiquidSolidVapor&gt, then the stable phases are liquid, inert solid and vapor and the vapor / liquid phase equilibrium is calculated with an equation of State and it must be connected to three outlets of appropriate type to receive those phases.

In any case the inlet pressure is the minimum between the pressures of all feeds.

Separator

Separator is a model to represent a generic and isothermal-isobaric separator with an unlimited number of feeds and at least two outlets; separation yields are fixed by means of the outSplit two-dimensional array, indexed by the ordinal of the exit and by the ordinal or the identifier of the component.

The stage cuts must be positive (if negative values are specified, they are clipped to be zero).

The columns of the array outSplit must be normalized, i.e. for any given component, the sum of the stage cuts to all outlets must be unitary.

If the user specifies the stage cuts for a component to the first N-1 outlets with a sum greater than one, then all the stage cuts to that component are renormalized.

If the sum of the stage cut for a component to the first N-1 outputs is less than or equal to 1, then the stage cut to the last exit (N) is obtained by difference.

Therefore it is preferable to specify the stage cut already normalized to all components, and for all outlets except the last one.

For example the separation of hydrogen in Palladium metal membrane represented as a named Separator unit named SEPARATOR with two outputs, with ordinals 0 (hydrogen-enriched stream) and 1 (stream impoverished in hydrogen), can be set as follows:

for (int i=0; i<NCOMPONENTS; ++i)
  if (components[i]->id() != "H2")
    *Q("SEPARATOR.outSplit", 0, i) = recovery_others;
*Q("SEPARATOR.outSplit", 0, "H2") = recovery;

Flowsheeting

Flowsheeting is a simulation technique for continuous industrial processes that represents the unit operations as the vertices of a directed graph, and the streams as the oriented arcs of the same graph.

With this approach very different problems are treated in a homogeneous way, and the path of the solution, and other properties can be found automatically thanks to graph theory.

To create a new flowsheet in LIBPF® the steps are:

  1. Derive a class from the FlowSheet type;

  2. Instantiate the type_ variable

  3. Define the constructor where connectivity is set;

  4. Define the setup method where you set the default values of operating parameters;

  5. Register the type with the object factory

  6. Instance an object of the type you just created;

  7. Use it.

Class declaration

All flowsheets should derive from the FlowSheet type.

Start from this standard code, copy and paste it and adapt it to your needs:

class MyFirstFlowSheet : public FlowSheet {
private:
  static std::string type_;
public:
  MyFirstFlowSheet(Libpf::User::Defaults defaults, uint32_t id=0, Persistency *persistency=nullptr, Persistent *parent=nullptr, Persistent *root=nullptr);
  const std::string &type(void) const { return type_; }
  void makeUserEquations(std::list<Assignment *>::iterator &p) { }
  void setup(void);
}; // SaltPepper

The type_ variable

The type_ static member and the associated public function type() are compulsory to allow reflection and persistence of instances.

They associate a string (type_) to the type of the class; in this way it becomes possible to register the type with the object factory, etc.

The variable type_ being static must also be instantiated:

std::string MyFirstFlowSheet::type_("MyFirstFlowSheet");

Note: you could also could use the default typeinfo functionality of the C++ language, but typeinfo returns different strings for the same class depending on the implementation of the language, which is not practical. Also defining a type_ string allows in certain cases to shorten the string, for example for the type:

MultiStage<FlashSplitter<StreamIdealLiqudiVapor>, StreamLiquid, StreamVapor>

we could define the type_ as:

MultiStage<FlashSplitter<VL_ideal>,L,V>

The constructor

In the constructor the connectivity is set, specifying the units and how the streams are connected.

For example:

MyFirstFlowSheet::MyFirstFlowSheet(Libpf::User::Defaults defaults, uint32_t id, Persistency *persistency, Persistent *parent, Persistent *root) :
  Model(defaults, id, persistency, parent, root),
  VertexBase(defaults, id, persistency, parent, root),
  FlowSheet(defaults, id, persistency, parent, root) {
  if (!persistency) {
    diagnostic(2, "Define unit operations");
    addUnit("Mixer", defaults.relay("MIX",  "Mixer"));
    addUnit("FlashDrum", defaults.relay("RX",  "Reactor")
                                       ("nReactions", 1)
                                       ("embeddedTypeReactions[0]", "ReactionYield"));
    addUnit("Separator<2>", defaults.relay("FILTER",  "Belt filter"));
    addUnit("Divider", defaults.relay("SPL",  "Three-way valve")
                                     ("nOutlets", 2));

    diagnostic(2, "Define stream objects and connect");
    addStream("StreamIdealLiquidVapor", defaults.relay("S01", "Feed"), "source", "out", "MIX", "in");
    addStream("StreamIdealLiquidVapor", defaults.relay("S02", "Reactor feed"), "MIX", "out", "RX", "in");
    addStream("StreamIdealLiquidVapor", defaults.relay("S03", "Reactor product"), "RX", "out", "FILTER", "in");
    addStream("StreamIdealLiquidVapor", defaults.relay("S04", "Product"), "FILTER", "out1", "sink", "in");
    addStream("StreamIdealLiquidVapor", defaults.relay("S05", "Mother liquor"), "FILTER", "out2", "SPL", "in");
    addStream("StreamIdealLiquidVapor", defaults.relay("S06", "Recycled mother liquor"), "SPL", "out1", "MIX", "in");
    addStream("StreamIdealLiquidVapor", defaults.relay("S07", "Purged mother liquor"), "SPL", "out2", "sink", "in");
  }
} // MyFirstFlowSheet::MyFirstFlowSheet

Units are added with the FlowSheet::addUnit function

while streams are connected with the FlowSheet::addStream function.

In all flowsheets there are also the source and sink Terminator units, whose purpose is respectively to be the source of al feed streams to the flowsheet and to collect all the outlet streams from the flowsheet. These are builtin and should not be created by the user.

The setup method

The objective of the setup method are:

Example:

void MyFirstFlowSheet::setup(void) {
  // Call FlowSheet::setup to initialize any subflowsheet
  FlowSheet::setup();

  // Set input variables
  S("S01.flowoption") = "Mw";
  Q("S01.P").set(110000.0, "Pa");
  Q("S01:Tphase.mdot").set(1000.0, "kg/h");
  my_cast<Stream *>(&at("S01"),CURRENT_FUNCTION)->clearcomposition();
  Q("S01:Tphase.w", 0).set(0.95);
  Q("S01:Tphase.w", 1).set(0.05);
  Q("S01:Tphase.w", 2).set(0.0);
  at("RX:reactions", 0).I("keycomp") = 1;
  at("RX:reactions", 0).Q("coeff", "Water") = 0.0;
  at("RX:reactions", 0).Q("coeff", "Table Salt") = -1.0;
  at("RX:reactions", 0).Q("coeff", "Pepper") = 1.0;
  at("RX:reactions", 0).Q("z").set(0.9);
  Q("FILTER.outSplit", 0, "Water").set(0.002);
  Q("FILTER.outSplit", 0, "Table Salt").set(0.002);
  Q("FILTER.outSplit", 0, "Pepper").set(0.99);
  Q("SPL.outSplit", 0).set(0.95);

  // Define cut streams
  addCut("S06");

  // Making selected outputs visible in UI
  Q("FILTER.outSplit", 0, "Water").setOutput();
  Q("FILTER.outSplit", 0, "Table Salt").setOutput();
  at("RX:reactions", 0).Q("conv").setOutput();
  Q("S06:Tphase.mdot").setOutput();
} // MyFirstFlowSheet::setup

In the code shown it makes extensive use of methods at() and Q() to access the sub-objects and variables at run-time, in particular the reactions inside the reactor and within phases of the streams.

Instantiate and use the flowsheet

The following instructions are standard and can be used to add and calculate virtually every flowsheet:

MyFirstFlowSheet flo(Libpf::User::Defaults("test", "test flowsheet"));
flo.check_inout();
flo.setup();
flo.check();
flo.calculate();
std::cout << flo;

Sequential or simultaneous resolution

By default flowsheets are solved sequentially first, then simultaneously.

The FlowSheet::supportsSimultaneous methods is virtual and can be overridden; it returns true if flowsheet can be solved in simultaneous mode and it defaults to true.

If a flowsheet overrides it to return false, then the simultaneous resolution is disabled:

bool MyFirstFlowSheet::supportsSimultaneous(void) { return false; }

Additional equations: feed-backs and process specifications

FlowSheet::makeUserEquations is a mandatory method in a flowsheet, but often (as in the example above) its implementation is trivial: { }.

It is used only when required, to add additional equations such as feed-backs and process specifications to the flowsheet. These equations are added in a way that is compatibile with both the sequential and the simultaneous resolution.

For example if we want to add this additional equation:

a = b

this has to be written inside FlowSheet::makeUserEquations as follows:

MAKEASSIGNMENT(a, b, "equate a and b")

During the sequential resolution, the usual assignment is evaluated. It is then a requirement that the first parameter is valid L-value.

Fo this reason these formulations are invalid:

MAKEASSIGNMENT(a - b, 0, "equate a and b") <==== ERROR
MAKEASSIGNMENT(0, a - b, "equate a and b") <==== ERROR

During the simultaneous resolution, what previously was the L-value (the a variable) becomes a new unknown, and a new open equation is added:

f(a) = a - b = 0

Sometimes though you might want to add equations that can not easily be turned explicit in the unknown, such as:

sin(a/b) = b

where a is again the unknown.

In this case you need to rewrite the equation in order to transform it into an explicit assignment in a, such that if this assignment is executed repeatedly, it iteratively converges.

This reformulation must be made using some intuition of the process, and it is not easy to give a standard methodogy.

We present three examples.

  1. Set the heat output developed by the burner (power) as a certain value (setPower) acting on the power supply of fuel (FUEL:Tphase.ndot); in this case:

     lhs = *Q("FUEL:Tphase.ndot")
     rhs = *Q("FUEL:Tphase.ndot") * (setPower / power)
    

    in some cases there may be convergence problems especially if setPower and power differ considerably (power/setPower very different from 1); if this is the case, then you can make it act more gradually by damping it with a square root function:

     rhs = *Q("FUEL:Tphase.ndot") * sqrt(setPower / power)
    

    or perhaps to an even lower exponent:

     rhs = *Q("FUEL:Tphase.ndot") * pow(setPower / power, 0.2)
    
  2. Determination of the exchange area (HX.A) required to reach a certain temperature (Tset) in a stream output from the hot side of the exchanger (Hotin.T)

     lhs = *Q("HX.A")
     rhs = *Q("HX.A") * ( *Q("Hotin.T") - Tset) / ( *Q("Hotin.T") - *Q("Hotout.T"))
    

    Increasing the area the temperature difference (Hotin.T - Hotout.T) will increase until it reaches the desired heat jump (Hotin.T - Tset); if the area is too large, the temperature difference is too high:

     (Hotin.T - Hotout.T) > (Hotin.T - Tset)
    

    and the assignment will drive the area back.

    Note how there is not need to take the absolute values of the temperature differences since they should be positive at all times

    If the temperature were to be imposed on the cold side, the ratio would be reversed (and for readability also the temperature differences should be reversed, to ensure they are all still positive):

     rhs = *Q("HX.A") * ( *Q("Coldout.T") - *Q("Coldin.T")) / (Tset - *Q("Coldin.T"))
    

    Also in this case it may be advantageous to dampen the dimensionless factor which multiplies HX.A, as in:

     rhs = *Q("HX.A") * pow(( *Q("Hotin.T") - Tset) / ( *Q(" Hotin .T") - *Q("Hotout.T")), 0.1)
    
  3. Set the percentage of the recycled water in stream RECY:Tphase.w[H2O] by varying the stage cut SEP.outSplit[0][H2O] in a component separator:

     lhs = *Q("SEP.outSplit", 0, "H2O")
     rhs = *Q("SEP.outSplit", 0, "H2O") *(1.0 + (0.77 - *O("RECY:Tphase.w[H2O]")) / *Q("RECY:Tphase.w[H2O]"))
    

    In this case if RECY:Tphase.w[H2O] < 0.77 the dimensionless factor will be slightly greater than one, so that the stage cut of the component will be increased.

References