3.2.4. Modules |
The
Module Type
A
Module Instance
Module
Parameter
Predicate
Overloading
Module
Overloading
Module
Properties
Modelling based on petri nets can result in large and confusing complex structures. The fear of generating confusing and hardly understandable models is often used as an argument against net-based describing methods. Other describing methods which are not based on the net theory, also create complex structures without imaging them in a single representation. Often model structures are used which mainly have physical references to the real system but their informative references are often represented by other means such as decision tables.
The experience in modelling with nets proves that models remain clear and understandable when working with 6 to 10 predicates and 6 to 10 transitions. This results in approximately 30 to 40 arcs. If this number is exceeded greatly, the above mentioned effect is inevitable. But this is not only true for the generation of simulation models but also for other software development. Therefore modularization is an essential programming concept for large applications. With this concept a total complex system is divided into several modules which can be implemented independently of each other and remain understandable. Their interaction, their functionalities and their interfaces are described accordingly.
The concept of modularization provides two essential advantages regarding the development of simulation models:
The behaviour of partial processes can be described by means of clear modules. | |
A partial behaviour, when described once, can be reused everywhere according to its functionality. |
3.2.4.1. The Module Type |
Poses++ provides a concept of module types similar to a C++ class data types with declarations of parameter, behaviour and interfaces. A module is applicable as a type and can be reused in other modules. The resulting language properties are very similar to those of the type concept of C++, so that Poses++-modules support typical object-oriented methods such as data packaging, connection of data and rules in a type.
Only inside a module type the declaration of predicates and transitions with their arcs is allowed.
The declaration of a module starts with the keyword module followed by the name of that module type. Within a module type a distinction is made between externally visible (public) and invisible (private) areas. Instances of other module types can only use declarations or access parameters public area. If no decision for a public or a private section was made for all declarations in a module type public is assumed automatically.
module ModuleTypeName { private: int PrivateParameter; // only visible inside public: int PublicParameter; // visible for all private: place PrivatePlace; trans PrivateTransition { PrivatePlace >> $ << $; } };
A Poses++ source file contains typicaly a module type with the same name (e.g. clock.pos contains the module type clock). But declaring more than one module types in one source file is as well allowed as you can assign the module types the names you want following the C-like conventions for type names (leaded by a letter and following letter, digit and underscore characters).
To make the module types from one source file visible and accessible for module types in other source files you should use the import keyword before any other declarations. When you import a source file all other source files imported into that source file are implicitly visible to you. To make e.g. C/C++ functions from a header file visible in a Poses++ source file you can use the C-like #include filename directive. Type declarations are possible on global scope (outside any module declaration) and in module scopes.
import clock; // imports all declarations from clock.pos #include "posanim.h" // includes the text from posanim.h here struct GlobalStruct { // GlobalStruct declared on global scope int a,b,c; }; module ModuleType { // ModuleType declared on global scope struct LocalStruct { // LocalStruct on module scope int d,e,f; }; };
To access embedded types like LocalStruct
from outside the module
ModuleType
the scope resolution operator :: has to be
used (e.g. ModuleType::LocalStruct
).
3.2.4.2. A Module Instance |
All modelling specifications in Poses++ have to be developed on the base of module types. You can use module types as instances in other module types and you can select a module type to be the model for an experiment (see also: Miscellaneous Configurations).
An instance is a member of a structured type (struct, module) by itself based on a data type. So you can construct structured types with members based on oridinal types (e.g. char, int, float) or also based on structured types. If inside a module declaration a member based on a module type is specified we call this a module instance.
module A { }; module B { A a1; // a1 and a2 are in B module instances of type A A a2; }; module C { A a; // a is in C a module instance of type A B b; // b is in C a module instance of type B };
If you would build such a Poses++ source code to a model you can select to get A, B or C as the model. If you select A you will see nothing in your model but selecting B or C you will have the module instances inside B or C as sub modules. For your simulation experiment at least one module must contain all sub modules necessary for your model but in your source code you can also have some different variants of module types also suitable for experiments.
3.2.4.3. Module Parameter |
Defining a module type you are able to specify constant variables with initial values public for all other module types. Such constants you can treat as the parameter of a module type. At first you can specify the values of these parameters in the module type itself but you are also able to change these parameters when you instanciate this module type inside another module. Doing so the values of the parameters will be changed when the whole model structure will be created by the simulation server.
module Range { const int Max = 5; const int Min = 1; private: const int RangeWide = Max - Min; };
In case of Range
would be your model you would have
Max=5
, Min=1
and from Max and Min dependent
RangeWide=4
. Using the module type Range inside another module
type you can change the public accessible parameters which means the values
will be changed.
import Range; // if the module Range comes from range.pos module Area { Range LeftRight(Min = 0); Range TopDown (Min = 0, Max = 10); };
In module type Area two sub modules of module type Range are instanciated. The instance LeftRight changes the parameter Min from its original value 1 to the new value 0. The parameters can be changed in the parameter list of an instance with an expression where on left side of the assignment the identifier from inside the module has to be used and on the right side of the assignment the new value has to be specified. Parameters are changed by value what means the value of the right part of the assignment will affect the parameter at the time when the module instance is created.
3.2.4.4. Predicate Overloading |
A simple tree hierarchie of modules like constructable with module types and module instances is often not flexible enough to describe the structure, and thus, the behaviour of a model. To connect module instances together Poses++ supports on one hand the predicate overloading and on the other hand the module instance overloading which is sometimes more powerful.
Lets discuss
a simple clock example:
Your task is to model a clock - a simple clock with seconds, minutes and
hours. What do we need ? We should be able to count seconds, minutes and
hours and every time we reach a zero value for a counter the following and
dependent counter should be informed.
At first we need a counter able to count e.g. seconds, minutes or hours like
shown in the figure below:
module Counter { // (1) /* an unsigned integer because values less than 0 are not expected and the capacity of 1 can represent our actual counter value */ buffer<unsigned int,1> Digit << 0; private: match unsigned int value; trans Increment { Digit >> value << value + 1; } };
But this simple counter has no limit. It would count beginning from 0 continuously. To count seconds of a minute or minutes of a hour we need a limit.
module Counter { // (2) const unsigned int Limit = 60; // defaults to 60 (seconds, minutes) buffer<unsigned int,1> Digit << 0; private: match unsigned int value; trans Increment { Digit >> value,cond(value < Limit) << value + 1; } trans Zero { Digit >> Limit << 0; } };
The counter by itself work but how we can use this module as part of a clock model ? For a clock model we need three counter. One for the seconds, one for the minutes and one for the hours. But these counters must work in relation to each other. Only if the conter for the seconds reaches the value zero the counter for the minute should change its value one time. This can be reached by a trigger all changes will depend on a trigger.
module Counter { // (3) const unsigned int Limit = 60; // defaults to 60 (seconds, minutes) buffer<unsigned int,1> Digit << 0; place < 1> Trigger; private: match unsigned int value; trans Increment { Trigger >> $; Digit >> value, cond(value < Limit) << value + 1; } trans Zero { Trigger >> $; Digit >> Limit << 0; } };
If we would add an additional trigger not for the counter module itself but for a neighbour counter we would have all to build our clock.
module Counter { // (4) const unsigned int Limit = 59; // defaults to 0..59 (seconds, minutes) buffer<unsigned int,1> Digit << 0; place Trigger, Successor; private: match unsigned int value; trans Increment { Trigger >> $; Digit >> value, cond(value < Limit) << value + 1; } trans Zero { Trigger >> $; Digit >> Limit << 0; Successor << $; } }; module Clock { place ClockTrigger; Counter Seconds(Trigger = ClockTrigger); Counter Minutes(Trigger = Seconds.Successor); Counter Hours (Trigger = Minutes.Successer, Limit = 23); // 0..23 trans Trigger (parallel = 1, firetime = 1sec) { ClockTrigger << $; } };
By the assignment of Seconds(Trigger = ClockTrigger)
the place Trigger
in the module instance
Seconds
will be replaced or better
overloaded by ClockTrigger
. Such a replacement
of a predicate is done by reference what means not only the actual situation
of ClockTrigger
will be copied to
Seconds.Trigger
. Every time when the module
Seconds
acts with its predicate
Trigger
it will really act with the predicate
ClockTrigger
from its parent module.
And so the following model structure we have now:
3.2.4.5. Module Overloading |
Even more powerful than predicate overloading is the concept of module overloading. But the mechanism is very similar. Assigning a module instance in the parameter list of another module instance will replace the original one by the overloaded module. With this mechanism you are able to use central module instances for tasks (transitions) and you can use them also as predicate or parameter containers.
module A { ram<string> aString << "string in A"; }; module B { ram<string> aString << "string in B"; A a1_in_B(aString = aString); A a2_in_B; // aString in a1_in_B contains: "string in B" }; module C { ram<string> aString << "string in C"; A a(aString = aString); // aString in a contains: "string in C" B b(a2_in_B = a); /* "C.b.a2_in_B.aString" is identical to "C.aString" and "C.a.aString". The module a2_in_B in module instance b is overloaded with module a. */ };
Module overloading is useful to have near global access to parameter container or to central functionalities.
3.2.4.6. Module Properties |
Every module instance has the predefined properties name and path. The property name specifies the real instance name of this module instance and path the real full instance name including all parent names of that module instance. Both non writable properties are based on the data type string. Every predicate has the same predefined properties.
module Global { // a global container for all names and paths fifo<string> Names; fifo<string> Paths; }; module M { Global global; // public visible private: place<1> Initialized; trans Init { Initialized << $; global.Names << name; // inform about the instance name global.Paths << path; // inform about the instance path } }; module X { M mX; }; module All { Global global; // public visible M m1(global = global); M m2(global = global); X x (mX = m1 ); };
When you start this simple example the both Init transitions of the module instances m1 and m2 will fire before the simulation server reach the deadlock because nothing more is to do. The predicate All.global.Names will contain: "m1" and "m2". The predicate All.global.Paths will contain: "All.m1", and "All.m2". | |
For the module mX in All.X you will not find a name or a path string generated because mX was overloaded by All.m1. All overloaded module instances are marked by an @ character. The same indication also via the character @ is used to display the names of overloaded predicates. |
|