LIBPF® SDK Developer manual
Introduction
Intended audience
This document addresses those who wish to develop models with LIBPF® (model developers).
Scope
This document is the developer manual for LIBPF® (LIBrary for Process Flowsheeting) Software Development Kit (SDK) version 1.1;
Prerequisites
-
a basic knowledge of the C++14 programming language is required; the best reference is Bjarne Stroustrup “The C++ Programming Language” 4th ed. Addison-Wesley 2013
-
Read the LIBPF® Technology Introduction
Definitions
-
LIBPF®: (LIBrary for Process Flowsheeting), library developed in C++ language, which includes objects well-known to a process engineer (dimensional quantities, material streams, reactions, phase separators, heat exchangers, reactors, etc.). This library make possible the creation of executable applications specific for a particular continuous industrial process. It is oriented to a system modeling and not for detailed single unit operation modeling. Typical employment of the executable made with LIBPF® are:
- Interactive process flowsheeting (feasibility study, design or troubleshooting);
- Closed package distributed to third-people together with a paper flow-sheet and process documentation:
- Internal communication among various company functions as process database source;
- Key parameters and performance index calculation, also real time through DCS (Distributed Control System) or SCADA (Supervisory Control And Data Acquisition) interfaces and OPC (OLE for Process Control);
- On line process monitoring and advising.
-
SDK: Software Development Kit, collection of tools which make up the LIBPF® development system: developer documentation, compiled version in library form (.LIB .DLL for Windows environment or .a .so for UNIX environment) of the implementation and source form of the interface (header files .h for C++). The SDK lets the customization and compilation of the executable application exclusively on the original licensed system environment.
-
kernel: a set of process models packaged as a standalone calculation application or as a remotely-accessible service; supports a predefined set of components, fluid properties and model types, as defined by the model developer.
-
Workflow: work procedure which include thought, design, development, building, deployment and maintenance of the executable. The workflow differentiates four roles/actors:
-
LIBPF® developer: C++ developers who maintain and develop the LIBPF® SDK; the SDK is the deliverable;
-
Super-model developer: C++ advanced developer (deep knowledge of C++ programming language is strongly required) who implements new models of chemicals, fluids, unit operations; a fundamental model (core model) is the deliverable, and it is distributed as SDK add-on;
-
Model developer: C++ developer (general knowledge of programming language; a deep knowledge of C++ is not required) who build process models by bringing together the fundamental models (core models) of the SDK and the related add-ons; The dedicated process simulator is the deliverable (special purpose process simulator) in the form of a closed executable;
-
Model user: non-programmer user who applies the closed executable and interacts with it through the interactive user interface; any C++ knowledge is not required; Simulation results, sensitivity analysis and process studies are the deliverable.
-
Objects and classes
LIBPF® is based on a object-oriented approach, thus every real physical entity (object) has an instance of a simulated object. Common things among simulated object can be generalized in a class.
Class is a technical C++ term that defines a model for the object creation, which includes attributes and methods which are shared among all created objects.
The following table shows and example relation among real object, simulated objects and classes:
REAL OBJECT | SIMULATED OBJECT (INSTANCE) | C++ CLASS | |||
---|---|---|---|---|---|
Reaction | R101.reactions[0] | that is the 0-th element of the “reactions” vector of the “R101” reactor | |||
Flash (also reactive) with many inlets and one mixed phase outlet | Buffer D201, Reactor R100 | FlashDrum | |||
Flash (also reactive) with many inlets and one outlet for each separated phase | Reactor R200, Phase separator S105 | FlashSplitterDrum | |||
Chemical components | Methane, ethane, ammonia | ComponentDippr | |||
Biomass component | Woody biomass, Oily biomass | ComponentSolid | |||
Phase | Solid, vapor, liquid phase | Phase | |||
Material stream with two phases (vapor and solid) with ideal behavior | Feed, product, purge, makeup | StreamIdealSolidVapor |
Relations between classes
There can be three type of relation between classes:
-
is-a: by inheritance connection (derivation)
-
ReactionYield is a generic Reaction
-
has-a, consists-of: by composition
- the solid-vapor stream StreamIdealSolidVapor has two phases, one solid and one vapor. For each combination of phases there is a different class (StreamVapor, StreamLiquid, StreamIdealLiquidVapor, StreamIdealLiquidSolidVapor …)
-
uses-a: by delegation (needed for dynamic type definition):
-
the reactor uses the reactions, but it does not contain them: the reactor has a container of pointers to reactions; the reactions are instantiated by the object factory using a string list
-
the FlowSheet uses the unit operations but it does not directly contain them.
-
Note: it is possible to build a process model (deriving from FlowSheet) following “has-a” relations, but the unit operations need to be declared in the class declaration. For example:
class Jasper01 : public Model, public FlowSheet {
private:
static std::string type_;
public:
FlashDrum E101C; // Biomass pre heater cold side - biomass
FlashSplitterDrum<StreamIdealLiquidVapor> R101; // Gassificatore monostadio
FlashDrum R102; // Tar cracker
FlashDrum E102H; // Air pre heater hot side - syngas
FlashDrum E101H; // Biomass pre heater hot side - syngas
FlashSplitterDrum<StreamIdealLiquidVapor> E103; // Vapor generator
FlashDrum ICE; // Internal combustion engine
Compressor B101; // Air blower
FlashDrum E102C // Air pre heater cold side - air
StreamSolid S02; // Alimentazione biomassa
StreamSolid S03; // Biomassa preriscaldata
StreamVapor S06; // Raw syngas depolverato
StreamVapor S07; // Syngas clean and very hot
StreamVapor S08; // Syngas hot
StreamVapor S09; // Syngas quite hot
StreamLiquid S10; // Condensate
StreamVapor S12; // Syngas cold
StreamVapor S13; // Air feed to gasification
StreamVapor S15; // Air feed to gasification compressed
StreamVapor S17; // Air feed to gasification hot
StreamSolid S21; // Ash to solid treatment
StreamVapor S28; // Flue gas
StreamVapor S29; // Air to ICE
Jasper01(long cid);
void setup(void);
const std::string &type(void) const { return type_; }
}; // Jasper01
In practice, this approach is not practical, and a between the FlowSheet and the streams / unit operations a class relation “uses-a” is preferred.
Creating new classes
Model instances are created by instantiating classes, so when you need a custom model you need to create a class for that.
The first thing to do is to find a name for the new class. According to the identifier naming conventions in the LIBPF® Coding Standard class names should be upper CamelCase, and the order of the words should be by decreasing importance.
This constraint is mandatory for:
-
Phase
-
Stream
-
Reaction
-
RatingHeat
-
MultiReaction
-
RatingColumn
For flowsheets, this rule is not mandatory and the FlowSheet prefix is tipically skipped.
For example, these are suitable names for reactions: ReactionDecomposition, ReactionAaaaa, ReactionTest, while these are not correct: DecompositionReaction, reaction_aaaaa, Reactiontest.
Once you have chosen a suitable name, there are three step to follow to create the new class:
-
declaration written in the “header files” with the “.h” extension;
-
definition, implementation of the methods, written in the “source files” with the “.cc” extension;
-
instantiation: a simulated object is built up based on one class and it starts to exist in memory (lifetime).
Declaration example:
class ReactionReformingC7H16 : public ReactionYield {
private:
static std::string type_;
public:
ReactionReformingC7H16_Ref(Options options);
virtual const std::string &type(void) const {
return type_;
}
}; // class ReactionReformingC7H16
Definition example:
std::string ReactionReformingC7H16::type_("ReactionReformingC7H16");
ReactionReformingC7H16::ReactionReformingC7H16(Options options): PersistentInterface(options.id()), Persistent(options.id()), ModelInterface(options.id()), ReactionYield(options.id()) {
I("keycomp")->set_val(components.lookup("tar"));
*Q("coeff", "tar") = -1.0;
*Q("coeff", "H2O") = -7.0;
*Q("coeff", "CO") = 7.0;
*Q("coeff", "H2") = 15.0;
} // ReactionReformingC7H16::ReactionReformingC7H16
In the simplest model, at least the following members/methods must be defined:
-
the constructor;
-
the static member type_ and the public static function type(), which are required for the reflection (the type_ usually coincides with the class name; abbreviations are allowed, e.g. the MultiStage<FlashSplitterDrum<StreamNrtl1LiquidVapor>, StreamLiquid, StreamVapor> class has type_ MultiStage<FlashSplitterDrum<VL_NRTL1>,L,V>.
The FlowSheet objects require also the following function (but the dummy { } implementation is sometimes given):
-
void setup(void) must contain the default values and the complex initializations which can not be inserted in the class constructor, or that require some options which are run-time user specified after the construction; It is called right before the first calculation
-
void makeUserEquations(std::list<Assignment *>::iterator &p)
LIBPF® already has a large number pre-defined classes, declared in the respective header files (“phases.h”, “streams.h”, “units.h”, “reactions.h”).
The corresponding definitions are already compiled within the library files (“libPF.lib” and the debug version “libPF_D.lib”). In order to use such classes, it is sufficient to call (include) the header file and then instantiate the object.
Instantiation and Object Factory
There are two ways you can instantiate an object of a certain type:
-
static instantiation: if the type is a C++ class and it is known at compile time:
ReactionOxidationC myReaction(Libpf::User::Defaults("r1", "carbon oxydation reaction"));
-
dynamic instantiation: via the object factory if the type is identified by a string known at run-time:
Node *node = nodeFactory.create("ReactionOxidationC", Libpf::User::Defaults("r1", "carbon oxydation reaction"));
or in case of a composed object (“use-a” case):
FlashDrum R101(Libpf::User::Defaults("R", "Equilibrium reactor") ("nReactions", 5) ("embeddedTypeReactions[0]", "ReactionCHNO_9") ("embeddedTypeReactions[1]", "ReactionOxidationC") ("embeddedTypeReactions[2]", "ReactionReformingEquilibriumCH4") ("embeddedTypeReactions[3]", "ReactionSynthesisEquilibriumNH3") ("embeddedTypeReactions[4]", "ReactionWaterGasShiftEquilibrium"));
In this case, the R101 reactive flash drum is declared at compile time, but the details about how it is build are defined at run-time.
In particular, the reactions that take place in R101 are defined using a currying syntax supplying the nReactions integer parameter and the values of the required elements in the embeddedTypeReactions string vector.
At run-time, the constructor of R101 (FlashDrum::FlashDrum) will instantiate five reactions of type ReactionCHNO_9, ReactionOxidationC, ReactionReformingEquilibriumCH4, ReactionSynthesisEquilibriumNH3 and ReactionWaterGasShiftEquilibrium.
Ultimately it is the constructors of those reaction classes that are called, but to decide which constructor to call based on the supplied strings, an object factory is required.
The object factory is a particular object which instantiates an object of a certain class when is given the class name as a string:
string “ReactionReformingC7H16” → Object Factory → instance of an object of type ReactionReformingC7H16
The object factory must be manually instructed about the association between strings and classes; this association is already set for the predefined classes in LIBPF®, and must be set by the user for the new classes.
Instructions on how to pass this information to the [object factory+(sdk_reference/class_node_factory.html) typically take this form:
NodeFactory nodeFactory;
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
the string that you pass as an argument to the function NodeFactory::registerType must be the same that is put into the static member type_ for that class.
Creating model instances
LIBPF® models are designed to be built from a saved state in the persistency database, or from scratch.
In the first case, the only parameter that must be passed to the constructor is the database id of the object that must be retrieved - all the rest will be loaded from the database (the database id is an interger value returned by the Node::insert function when the object was inserted in the database).
In the second case the parameters that must be passed to the constructor are the tag and description, plus all the options (strings and integers) required to fully configure the construction of the object itself and of its sub-objects.
For example, a reactor could have as options the reactions:
reactions = {“ReactionOxidationH2”, “ReactionOxidationCO”}
Alternatively a distillation column might have 10 equilibrium stages and feed to the 5th plate, requiring two integer values for nStages and nFeed:
nStages = 10
nFeed = 5
To handle these two use cases you would need two different constructors:
Reactor::Reactor(std::string tag,
std::string description,
std::list<std::string> reactionList = {"ReactionOxidationH2",
"ReactionOxidationCO"});
Column::Column(std::string tag,
std::string description,
std::list<std::string, std::int> options = { {"nStages", 10}, {"FeedStage", 5}});
In addition you would need a constructor taking the database id as the only integer parameter, to handle the case when you want to build from a saved state:
Reactor::Reactor(int id);
Column::Column(sint id);
However, providing constructors with all those signatures would be impractical.
Although C++14 supports delegating constructors (or chained constructors) that allow to designate a default constructor that does all the hard work, the object factory would still have to support all possible signatures !
The solution used in LIBPF® instead relies on a single constructor signature (this signature has been simplified in this example):
Reactor::Reactor(Defaults defaults, int id);
Column::Column(Defaults defaults, int id);
The Defaults object allows you to pass all the options that should be used to create an object from scratch, including tag and description. Furthermore the Default object can be appended any number of additional options, using a currying syntax exploiting overloads of the:
operator()
The vector options are passed in a “flattened” way, where arrays of options are passed as an integer setting the size of the array, and the list of array elements.
The list of reactions above is represented by the value 2 is assigned to the integer parameter nReactions and by two values in the array of string embeddedTypeReactions:
nReactions = 2
embeddedTypeReactions[0] = "ReactionOxidationH2"
embeddedTypeReactions[1] = "ReactionOxidationCO"
Using this formalism the calls to construct the objects of the two previous examples are expressed in LIBPF® in this way:
Reactor reactor(Defaults("reactor", "reactor description")
("nReactions", 2)
("embeddedTypeReactions[0]", "ReactionOxidationH2")
("embeddedTypeReactions[1]", "ReactionOxidationCO"));
Column column(Defaults("column", "column description"))
("nStages", 10)
("FeedStage", 5));
If the two objects must be reconstructed from persistent storage, and assuming that id in the database equal to 777 and 888, the calls will be respectively:
Reactor reactor(Defaults, 777);
Column column(Defaults, 888);
The Defaults object also provides functionality to forward options to sub-objects, and setting default values.
The typical scenario in LIBPF® is that the top-level object, of type derived from the class FlowSheet, contains streams and unit operations, some of which may in turn be of types derived from the class FlowSheet and contain other objects.
If at the time of construction of the top-level object you want to set an option that must apply to a certain object contained in this hierarchy, all the constructors must properly forward that option.
For example, suppose a type object contains two objects, aa of type Aa and ab of type Ab; the latter in turn contains the sub-objects aba, abb and abc of respective types Aba, Abb and Abc. Let’s assume that you want to pass the option nStage = 2 to the abc.
The call to the constructor of the highest level will have this form:
A a(Defaults("", "")("nStage", 2));
The A constructor will forward the nStage option to all its sub-objects and through to all sub-sub… objects, possibly overwriting any default value it may have.
Note: in a future release of LIBPF® a filter functionality will be implemented, which will send the options only selectively to certain sub-objects. In the preceding example, if you woulf prefer to forward the nStage = 2 option only to the abs sub-sub-oobject, rather than to all sub-objects, the syntax would become:
A a(Defaults("", "")("ab:abc:nStage", 2));
As for default values, if an option is not specified, the value is -1 for the integer options and the empty string "" for the string options.
All the examples until now have offered insight into the user of built-in or custom models. But how does this option-passing mechanism impact model developers ?
First of all as a model developer you are not likely to instantiate the models directly, but rather though the object factory or even more likely using the FlowSheet::addUnit and FlowSheet::addStream member functions.
Using those functions the reactor and column instantiations become:
addUnit("Reactor", Defaults("reactor", "reactor description")
("nReactions", 2)
("embeddedTypeReactions[0]", "ReactionOxidationH2")
("embeddedTypeReactions[1]", "ReactionOxidationCO"));
addUnit("Column", Defaults("column", "column description"))
("nStages", 10)
("FeedStage", 5));
Note how the type is passed as first argument, but the subsequent arguments are unchanged w.r.t. the direct command.
Second, any constructor must assume that it may be called during the construction of a higher-level object. Even if you are writing a complex process model of a gas cleanup, it may happen that later somebody else integrates it in an even larger model.
For this reason, if the model developer wants to set a default value, but at the same time allow the model user overwriting it, she can use the relay function, which returns a copy of the Defaults object, with overridden tag and description.
This allows the options set at a higher level to be propagated to the sub-objects. Using Defaults::relay the reactor and column instantiations become:
addUnit("Reactor", defaults.relay("reactor", "reactor description")
("nReactions", 2)
("embeddedTypeReactions[0]", "ReactionOxidationH2")
("embeddedTypeReactions[1]", "ReactionOxidationCO"));
addUnit("Column", defaults.relay("column", "column description"))
("nStages", 10)
("FeedStage", 5));
Abstract or semi-abstract objects ( mixins ) that are used to implement some of the functionality of complex objects, but which do not contain variables or options that have to be retrieved from persistency can have constructors with simpler signatures.
These simpler constructor signatures can be used for any class that will never be instantiated by the ObjectFactory.
Errors and warnings
If while running (run-time) a recoverable error is encountered (i.e., that allows to continue the calculation), this is recorded in the unit operation or in the stream that has caused.
If the error is unrecoverable an exception is thrown (see Exceptions).
Recoverable errors can be of two types:
-
Warnings signal anomalies to be treated with caution, but the results may still be correct. Warnings do not stop an homotopy calculation.
-
Errors do not block the normal calculation, but the results are no longer reliable. Errors do stop the execution an homotopy calculation.
Browsing objects and sub-objects
Because the objects actually instantiated at run-time are dynamic (i.e. they have a uses-a relationship with sub-objects), it is not possible to identify at run-time some member or sub-object using only the C++ operators . (dot) and -> (arrow).
Everything would be easier if a flowsheet had a reactor member (has-a relationship) with in turn some reaction members (has-a relationship) (Note: non-working pseudo-code):
class Reaction {
public:
double z;
};
class Reactor {
public:
Reaction R1, R2;
};
class Flowsheet {
public:
Reactor R100;
};
Flowhseet F;
std::cout << F.R100.R
However, if the flowsheet and the reactor are constructed dynamically (and then use the uses-a relationship with the reactor and the reactions respectively ), there will be no object named R100 within F or R1 and R2 objects inside of R100, but there will be only unnamed pointers, as in:
class Reaction {
std::vector<double *> variables;
};
class Reactor : public Unit {
std::vector<reaction *> reactions;
};
class Flowsheet {
std::vector<Unit *> units;
};
The LIBPF® library provides access methods common to all models (they are methods of the semi-abstract class Object) that allow you to browse the variable members (Quantities, Strings and Integers) and the sub-object members. This functionality is reflection, i.e. the capability of a computer program to observe its own type structure (types, sizes, member layout, member function signatures, inheritance) at run-time.
In the preceding example, the variable z in the first reaction of reactor R100 could be found with the statement:
std::cout << F.at("R100:R1").Q("z");
or more compact:
std::cout << F.Q("R100:R1.z");
Compared to native C++ syntax, there are some differences:
-
As a separator between objects and sub-objects LIBPF® uses the colon
:
while the period.
separates the object from the variable; in native C++ syntax the point is always used -
Handling of multidimensional members: members may be scalars (temperature, flow), but also vectors (compositions, capacities for each component, vectors of objects) or matrix (stoichiometric coefficient matrices of multi-reactions or arrays of objects); multidimensional members are indexed by integers or component identifiers
-
whereas the native C++ syntax that differs markedly vectors from associative maps, arrays in LIBPF® can be indexed either by strings (the names of the components) or by integers (the ordinal of the component in the list of components). Unlike C++, the strings with the names of the components are not to be included in quotation marks.
-
in the case of arrays LIBPF® uses the FORTRAN-like syntax
[i, j]
instead of C++ syntax[i][j]
For example, if the list of components is: [H2, H2O, O2]:
Q("reaction[0].z")
Q("x[H2]")
Q("mdotcomps[H2O]")
Q("coeff[0,O2]")
or:
Q("x[0]")
Q("mdotcomps[1]")
Q("coeff[0,2]")
The Q, S, I, and at functions can be invoked at any level of the hierarchy of objects and objects, and the rule of scoping applies.
Always referring to the previous example:
- ```F.at("R100")``` returns a reference to the reactor R100
- ```F.at("R100").at("R[0]")``` or ```F.at("R101:R[0]")``` returns a reference to the first reaction within reactor R100
- ```F.at("R100:R[0]").Q("z")``` or ```F.at("R100").Q("R[0].z")``` or ```F.Q("R100:R[0].z")``` returns a reference to the variable z in the first reaction inside the reactor R100.
The reflection mechanism requires each Object to hold internal maps between string tags and variables / sub-objects. All automatic variables and all sub-objects must be registered with those maps.
To make sure automatic variables are registered for the reflection mechanism, the default, empty-list constructor must be disabled for all variable types.
In this way member variables must be set up (tag, description, parent=this and default value) in the constructor initializer list.
Adding them the maps used for reflection must be done in the constructor body with the overloaded addVariable member function:
Phase::Phase(...) : mdot("mdot", "mass flow", this) ... {
addVariable(mdot);
...
}
To avoid repeating the variable name twice (as identificator and as string passed to the tag constructor parameter), and repeating the this parameter, syntactic sugar in the form of a DEFINE macro is provided:
#define DEFINE(tag, description) tag(#tag, description, this)
With this, the Phase constructor above can be written:
Phase::Phase(...) : DEFINE(mdot, "mass flow") ... {
addVariable(mdot);
...
}
Utility classes
Diagnostics
The diagnostic macro prints the diagnostic message passed as second argument to std::cout only if the sum of verbosityGlobal + verbosityFile + verbosityLocal + verbosityInstance is larger or equal to the verbosity level passed as first parameter.
The meaning of the four verbosity levels is set at compile time as follows:
-
verbosityGlobal is a global variable that holds for the entire executable, which can be directly manipulated ay any time
-
there is one verbosityFile constant static variable in each compilation unit
-
there is one verbosityLocal constant static variable in each function
-
there is one verbosityInstance variable in each class instance, and it can be manipulated via the setVerbosity() member function
The diagnostic message can contain strings and objects chained with the « operator as in a statement involving the std::cout stream (this exploits the lower priority of the « stream operator with respect to the comma operator).
Example:
diagnostic(0, "test: " << test);
The tipical verbosity levels are:
-
level 0: important messages that are printed during normal operation
-
level 1: more informative messages that are printed during operation with increased verbosity
-
level 2: detailed messages on entry into functions
-
level 3: detailed messages on intermediate results and exit from functions
Exceptions
If at run-time a fatal error is encountered, a C++ exception is thrown.
All exceptions thrown by the LIBPF® library are objects derived from the class Error: e.g. ErrorBrowsing for errors related to object browsing, ErrorUnit for errors related to units of measurement etc.
The main purpose of these classes is to report detailed error information.
To facilitate the analysis of errors, the main stuff is typically included in a try / catch block:
try {
// main stuff
} catch {...}
When a function catches an exception from a function call, it appends to the error object that receives from the exception some additional information about the current function, and then rethrows the exception.
For example:
void SampleClass::sampleMethod(void) {
try {
// main stuff
} // try
catch (Error &e) {
e.append(CURRENT_FUNCTION);
throw;
} // catch
} // SampleClass::sampleMethod
The stack unwinding is finally performed by the main function, which reports the detailed informations to std::cerr:
int main() {
try {
// main stuff
return 0;
} // try
catch (Error &e) {
e.append(CURRENT_FUNCTION);
std::cerr << "****************************** Fatal LIBPF error! ******************************" << std::endl;
std::cerr << e.message() << std::endl;
return -1;
} // catch
} // main
A typical stack unwinding message looks like this:
***************************** Fatal LIBPF error! *****************************
Error was thrown by function: GenericQuantity<valuetype>::operator=
Error type: Unit of measurement error - destination already has incompatible units: <kg s^-1> <kmol s^-1>
Called from function: SaltPepper::setup
Called from function: main
This informations should help locating the source of the problem.
Value
LIBPF® performs all mathematical operations on floating-point numbers using instead of the default C++ type double a special class Value that provides units of measurements and analytical derivatives.
Units of measurements are checked at run-time, and if any incorrect operation is tried an exception of type ErrorUnit is thrown; see LIBPF® supported units of measurements for a list of units.
Value is actually a typedef for GenericValue<valuetype> with valuetype == double.
It is possible (but currently not supported) to create objects of type GenericValue<float> for calculations in single precision floating point, GenericValue<std::complex> for calculations with complex numbers and GenericValue<std::vector <double>> for calculations with vectors.
Assignaments in model user code should preferably happen via the Value::set method:
Q("R101:reactions[2].conv")->set(0.3);
Q("S10:Tphase.mdot")->set(300.0, “kg/h”);
The Value::set method marks the variable as input, this will automatically make it visible in the User Interface.
Alternatively it is also possible to use the plain C++ assignment:
Q("R101:reactions[2].conv") = Value(0.3);
L-value = R-value
The C++ assignment is an asymmetrical binary operator: the left side must be an L-value while the right side can be an R-value - see Value (computer science); L-value and R-value must be of the same type that is in this case Value, or there must be an automatic conversion between the type of the R-value to the L-type, which is invoked implicitly if you write (dimensionless values):
Q("R101:reactions[2].conv") = 0.3;
The best practice is to avoid the implicit conversions and call explicitely the Value(0.3) constructor; in the case of values with dimensions, the explicit constructor is mandatory to avoid an exception be thrown for incompatible units of measurement.
Computation of derivatives
The 1st order derivatives can be calculated in one step together with the evaluation of each expression, with a technique known as FADOO (Forward Automatic Differentiation with Operator Overloading). The technique is described in the ADOL-C documentation, in the chapter describing the Tapeless forward differentiation.
Consider a function f: R^2 -> R^2.
When the function is evaluated numerically, it takes a vector of two floating point numbers for the independent variables [x_1, x_2], and returns the vector of two floating point numbers for independent variables [f_1, f_2].
To have in the same pass also the 1st order analytical derivatives with the FADOO approach, the function is fed with a vector of two more complex objects instead than simple floating-point numbers for each of the independent variables: for example, the first independent variable x_1 provides a data structure that contains a floating point number for the value of the variable, plus an array of floating point numbers with size equal to the total number of independent variables (in this case, 2) initialized with the derivative of the first variable w.r.t. to all other variables.
Finally:
x_1 = [value, [dx_1/dx_1, dx_1/dx_2] ] = [x_1_value, [1, 0]]
x_1 = [value, [dx_2/dx_1, dx_2/dx_2] ] = [x_2_value, [0, 1]]
At this point any mathematical operation performed to evaluate the function f must be overloaded so that it operates on these data structures in place of the simple floating-point numbers, and propagates the derivative information while doing the evaluation. In this way at the end of the evaluation of all expressions within f, the derivatives of the results w.r.t. the independent variables can be found in the results data structure.
This is an example calculation in LIBPF®:
BaseActive::rollContext(2); // this sets the number of derivatives to be calculated to 2
P.setActive(0);
T.setActive(1);
v = R * T / P;
diagnostic(0, "Molar volume with UOM = " << v);
diagnostic(0, "Molar volume with derivatives = " << v.printFull());
BaseActive::unRollContext();
This should be the output:
main * Molar volume with UOM = 24.4652 kmol^-1 m^3
main * Molar volume with derivatives = 24.4652 [-0.000241453 0.0820567 ]
which means that the derivative of the volume w.r.t. the 1st variable (the pressure) is -0.000241453 whereas the derivative of the volume w.r.t. the 2nd variable (the temperature) is 0.0820567.
How to get on
-
Read the LIBPF® SDK Classes overview
-
Browse the LIBPF® SDK C++ reference manual