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
-
acquaintance with the field of industrial continuous processes
-
Read the LIBPF® SDK Developer manual
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:
The fundamental scalar pure component parameters required to define a component are defined by the ComponentBase class:
-
molecular weight
-
critical data (pressure, temperature, compressibility, volume)
-
acentric factor
-
molar diffusivity parameter
-
molar volume at the boiling point
-
standard enthalpy of formation
-
standard Gibbs free energy of formation
Furthermore the way the properties are computed is determined by which mixins are assembled to compose a Component; mixins must be supplied for:
-
tramsport properties (RouteTransport)
-
vapor / liquid saturation properties (RouteSaturation)
-
caloric properties for the vapor / liquid / solid phases (RouteHeatVapor / RouteHeatLiquid / RouteHeatSolid)
-
volumetric properties for the vapor / liquid / solid phases (RouteVolumeVapor / RouteVolumeLiquid / RouteVolumeSolid).
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:
Significative phase classes are:
-
PhaseSimple (a template class parameterized with a SmartEnum that can be PhaseType::vapor, liquid or solid), is used to calculate only mass balances; does not track any properties and does not allow the predictive calculation of phase equilibrium; the flow rates are tracked only through mass-based variables, mdot, w and mdotcomps;
-
PhaseSimpleTotal is based on PhaseSimple and computes the total phase for multi-phase streams;
-
PhaseIdeal
(a template class parameterized with a SmartEnum that can be PhaseType::vapor, liquid or solid), is the most used phase type and employs the ideal models for mixture between the components and the calculation of fugacity; the flow rates are tracked in terms of molar-based variables (ndot, x and ndotcomps) as well as of mass-based variables (mdot, w and mdotcomps). -
PhaseTotal computes the total phase for multi-phase, molar- and mass-based streams.
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:
-
up to 1 pointer to the vapor phase (Vphase)
-
up to 3 pointers to liquid phases (Lphase1, Lphase2, Lphase3 or Lphase if only one is present)
-
up to 9 pointers to solid phases (Sphase1, …, Sphase9, or Sphase if only one is present).
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:
-
ideal laws (Raoult / Henry)
-
equations of state (cubic, PC-Saft and Gerg-2004)
-
or activity coefficient methods (NRTL).
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:
-
StreamSimpleLiquid: simple, mass-balance only stream with one liquid phase
-
StreamSimpleSolid: simple, mass-balance only stream with one solid phase
-
StreamSimpleVapor: simple, mass-balance only stream with one vapor phase
For a better integration in a flowsheet model, StreamOne-based types are suggested since they involve phase properties calculation:
-
StreamVapor: ideal stream with one vapor phase
-
StreamLiquid: ideal stream with one liquid phase
-
StreamSolid: ideal stream with one solid phase
-
StreamEosVapor: stream based on the Kwong-Redlich-Soave equation of state with one vapor phase
-
StreamEosLiquid: stream based on the Kwong-Redlich-Soave equation of state with one liquid phase
-
StreamNrtl1Liquid: stream based on the Non Random Two Liquids activity coefficient model in the original formulation with temperature-independent bij, with one liquid phase
-
StreamNrtl2Liquid: stream based on the Non Random Two Liquids activity coefficient model with temperature-dependent bij and alfaij, with one liquid phase
-
StreamPcsaftVapor: stream based on the PC-SAFT equation of state with one vapor phase
-
StreamPcsaftLiquid: stream based on the PC-SAFT equation of state with one liquid phase
-
StreamGerg2004Vapor: stream based on the GERG-2004 equation of state with one vapor phase see PhaseGerg2004 for limitations
-
StreamGerg2004Liquid: stream based on the GERG-2004 equation of state with one liquid phase see PhaseGerg2004 for limitations
-
StreamIapwsVapor: stream based on the IAPWS-95 equation of state with one vapor phase
-
StreamIapwsLiquid: stream based on the IAPWS-95 equation of state with one liquid phase
Two-phase streams
If a model requires a simple two-phase stream just for mass balances, the following stream types are available:
- StreamSimpleLiquidSolid: simple, mass-balance only stream with one liquid and one solid phase
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:
-
Using StreamVl, the generic two-phase stream has equilibrium factors (K_VL) independent from temperature and composition
-
StreamTabularLiquidLiquid: two-phase liquid-liquid stream with constant equilibrium factors
-
StreamTabularLiquidVapor: two-phase vapor-liquid stream with constant equilibrium factors
-
-
Using StreamVlT, the vapor-liquid stream has equilibrium factors (K_VL) dependent from temperature but not from composition
-
StreamIdealLiquidVapor: ideal stream with one liquid and one vapor phase
-
StreamIapwsLiquidVapor: stream based on the IAPWS-95 equation of state with one liquid and one vapor phase
-
-
Using StreamVlTx, the vapor-liquid stream has equilibrium factors (K_VL) dependent from temperature and composition
-
StreamNrtl1LiquidVapor: stream based on the Non Random Two Liquids activity coefficient model in the original formulation with temperature-independent bij, with one liquid and one vapor phase
-
StreamNrtl2LiquidVapor: stream based on the Non Random Two Liquids activity coefficient model with temperature-dependent bij and alfaij, with one liquid and one vapor phase
-
StreamEosLiquidVapor: stream based on the Kwong-Redlich-Soave equation of state with one liquid and one vapor phase
-
StreamPcsaftLiquidVapor: stream based on the PC-SAFT equation of state with one liquid and one vapor phase
-
StreamGerg2004LiquidVapor: stream based on the GERG-2004 equation of state with one liquid and one vapor phase
-
-
Using StreamLlTx, the liquid-liquid stream has equilibrium factors (K_LL) dependent from temperature and composition
-
StreamNrtl1LiquidLiquid: stream based on the Non Random Two Liquids (NRTL) activity coefficient model in the original formulation with temperature-independent bij, with two liquids
-
StreamNrtl2LiquidLiquid: stream based on the Non Random Two Liquids (NRTL) activity coefficient model in the original formulation with temperature-dependent bij and alfaij, with two liquids
-
The streams that do not calculate equilibrium involve one fluid phase and one solid phase (StreamOneSolid):
-
StreamIdealSolidVapor: ideal stream with one solid and one vapor phase
-
StreamIdealLiquidSolid: ideal stream with one liquid and one solid phase
-
StreamEosSolidVapor: stream based on the Kwong-Redlich-Soave equation of state with one solid and one vapor phase
-
StreamNrtl1LiquidSolid: stream based on the Non Random Two Liquids activity coefficient model in the original formulation with temperature-independent bij, with one liquid and one solid phase
-
StreamNrtl2LiquidSolid: stream based on the Non Random Two Liquids activity coefficient model with temperature-dependent bij and alfaij, with one liquid and one solid phase
Three-phase streams
If a model requires a simple three-phase stream just for mass balances, the following stream types are available:
- StreamSimpleLiquidSolidVapor: simple, mass-balance only stream with one liquid, one solid and one vapor phase
If phase equilibrium between the fluid phase is required, you need one of the StreamTwoSolid-based types:
-
StreamSolidVlT: three-phase stream with two fluid phases and one solid phase and with equilibrium factors (K_VL) dependent from temperature but not from composition
- StreamIdealLiquidSolidVapor: ideal stream with one liquid, one solid and one vapor phase
-
StreamSolidVlTx: three-phase stream with two fluid phases and one solid phase and with equilibrium factors (K_VL) dependent from temperature and from composition
-
StreamEosLiquidSolidVapor: stream based on the Kwong-Redlich-Soave equation of state with one liquid, one solid and one vapor phase
-
StreamNrtl1LiquidSolidVapor: stream based on the Non Random Two Liquids activity coefficient model in the original formulation with temperature-independent bij, with one liquid, one solid and one vapor phase
-
StreamNrtl2LiquidSolidVapor: stream based on the Non Random Two Liquids activity coefficient model with temperature-dependent bij and alfaij, with one liquid, one solid and one vapor phase
-
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:
-
PT (pressure-temperature)
-
PH (pressure-enthalpy)
-
PS (pressure-entropy)
-
PA (pressure-vapour fraction)
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>, 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:
-
Derive a class from the FlowSheet type;
-
Instantiate the type_ variable
-
Define the constructor where connectivity is set;
-
Define the setup method where you set the default values of operating parameters;
-
Register the type with the object factory
-
Instance an object of the type you just created;
-
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:
-
set the thermodynamic state, flow and composition for all feed streams
-
set the operating specifications for the unit operations
-
choose which additional interesting variables to display between the inputs and the results in the UI; for the latter it is automatic for variables that are set with the Value::set method.
-
optionally select streams to be cut and provide estimates for converging flowsheets with recycles.
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.
-
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)
-
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)
-
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.