This section describes the use of operations available in the library. Some of these operations (projection, input/output) have already been seen in the preceding sections, in the form of methods of the cogitant::Environment class. But these methods are merely shortcuts for using cogitant::Operation (and its sub-classes), this is why, in the frame of an advanced use of the library, it is better to know the use of this class in order to understand well the functioning of these methods corresponding to the operations defined based on the model, on input/output features as well as to define new operations. In this part, we explain the main library operations, and describe how to use these operations as instances of subclasses of cogitant::Operation and through "shortcuts" methods of cogitant::Environment.
The approach chosen to offer operations in the library is then to use objects instance of cogitant::Operation (or rather of its subclasses, because it is an abstract class). This solution has been preferred to the definition of methods in the cogitant::Graph or cogitant::Rule class as it offers more flexibility. Indeed, whether you have to define new operations (for example, managing a new file format) or you have to provide a new implementation of existing operations (for example a more effective projection algorithm), the solution of using cogitant::Graph methods require the definition of a new subclass, in which the method corresponding to the operation is redefined. This obviously raises the problem of the definition of several new operations: in this case, how to organize the inheritance relationship on (the) new class(es). On the contrary, the kept solution can define new operation classes that receive model objects as parameters. Thus, you can easily define new implementations of existing or new operations. In addition, the library handling references to instances of (subclasses of) cogitant::Operation, it is very easy to incorporate these new operations to the library, and to use methods of the library that will call, in an absolutely transparent way, new operations.
Generally, operations are used by instanciating an object of a subclass of cogitant::Operation, by calling setParamxxx() methods to fix the different parameters of the operation, then by calling the run() method which carries out the calculation. Finally, getResultxxx() methods can retrieve the result.
Conceptual graphs handled in Cogitant may be unconnected. However structuring a graph in connected components can be useful, this is why several methods of the library can determine whether a graph is connected or not, how many connected components of a graph and what are these connected components (vertices contained in each component). All this information can be calculated by calling cogitant::Environment::isConnected() (graph connectivity), cogitant::Environment::connectedComponentsSize() (number of connected components) and cogitant::Environment::connectedComponents() (connected components) methods. All these methods take as a parameter a (pointer to a) cogitant::Graph and a cogitant::iSet locating the cogitant::InternalGraph identifier whose connectivity must be determined (by default 0
, i.e. the graph of level 0). Indeed, in the case of nested graphs, a cogitant::Graph contains several "graphs," and if there is no interest in determining the number of connected components of all these graphs, it may be interesting to determine this information on one of them (and not just on the graph of level 0).
The disjoint sum is simply accessible from the cogitant::Environment::disjointSum() method which takes as parameters two (pointers to) cogitant::Graph and which modifies the first for adding it the second (which is not modified). This method actually uses the cogitant::OpeDisjointSum class, whose documentation describes the various features offered by this class that can make operations more complex than the simple disjoint sum (this operation is especially used when loading files when a reference to an already existing graph is done in a nesting (the graph must then be copied as nested graph, and coreference classes may be updated) and when applying a rule according to a projection (adding the conclusion and merging vertices, see below)).
Search of projections from a graph into another is done thanks to the cogitant::Environment::projections() method, to which one must pass the projected graph and the graph in which one projects. This method is actually overloaded and can take as parameters graph identifiers (cogitant::iSet identifying the graph in the environment) or pointers to cogitant::Graph. The third parameter of this method, a cogitant::ResultOpeProjection passed by reference, will contain, after the call, the result of the method execution. But this object can also set up the search. Indeed, according to the uses, one can be interested in various searches, which are more or less costly in time and memory. On can mainly distinguish 3 uses, which are presented here from the less to the most expensive one, and illustrated with an example.
false
as a parameter (to indicate that projections should not be stored), and cogitant::ResultOpeProjection::maxSize (cogitant::nSet) with 1
as a parameter (to indicate that the search must be stopped as soon as a projection is found). Both methods should be called on cogitant::ResultOpeProjection before calling a cogitant::Environment::projections(). Once this method has been called, it is necessary to call cogitant::ResultOpeProjection::isEmpty() to determine whether a projection was found. false
as a parameter before the call to cogitant::Environment::projections(). After the call, the method cogitant::ResultOpeProjection::size() allows you to know the number of found projections. Example.The example below shows the three cases presented above. The display of projections in the third case uses the output operator of cogitant::Projection which displays couples of vertex identifiers in graphs: the first element of each couple is the identifier (iSet) of a cogitant::GraphObject of the projected graph and the second element is the identifier of its image.
Projection iterator
With the use of a cogitant::OpeProjectionBundle and a cogitant::ProjectionIterator, projections are not calculated by a simple call (such as in the cogitant::Environment::projections() method): after an initialization by a call to cogitant::OpeProjectionBundle::begin(), a cogitant::ProjectionIterator calculates the next cogitant::Projection each time that its ++ operator is called. Please note that modifying the two graphs while projections are computed is strictly forbidden. Note also that an cogitant::OpeProjectionBundle can only calculate projections between two graphs at a time (even if two cogitant::ProjectionIterator are declared). So, in order to calculate simulaneaously projections between several couples of graphs, several cogitant::OpeProjectionBundle are needed, and they can be obtained by cogitant::Environment::newOpeProjectionBundle().
Example.
Changes in the projection search operation
Like all other methods of cogitant::Environment which give access to operations, cogitant::Environment::projections() is merely a shortcut that uses subclasses of cogitant::Operation. More specifically, this is the cogitant::OpeProjection class which takes charge of calculating projections. This operation searches projections by a backtrack algorithm that calculates in a first-time possible image lists (of each cogitant::GraphObject of the projected graph in cogitant::GraphObject objects of the graph in which we seeks projections), then filters these lists. Filtering is done by choosing a vertex o1 of the projected graph, and one of its images o2 among its list of possible images. By choosing this couple as part of the projection, this induces constraints on neighbors of o1: if there is an edge labeled i between o1 and o3, then o3 can have as images only vertices o4 such that there is an edge labeled i between o2 and o4. The list of possible images of o3 can then be filtered.
The cogitant::OpeProjection operation merely defines the main scheme of projection search. The run() method of this method actually makes calls to other operations, specialized in a task. Thus, to modify the projection search operation, you "just" have to write a subclass of cogitant::OpeProjection (at worst) or a subclass of one of the following operations to change a part of the search behaviour:
The "personalization" of the projection calculation can then be done by writing a subclass of one or more classes presented above. Obviously, in this case, it is necessary to specify to cogitant::OpeProjection that this new class should be used. For this, it is necessary to instantiate the new class and to pass an instance to the instance of the used cogitant::OpeProjection in order to inform it to use the new operation. But this requires an access to an instance of cogitant::OpeProjection. Therefore you have to instantiate this class and to provide it instances of all side operations, whether it is "standard" classes or new subclasses. This way of doing things is not very pleasant to use because it requires to instantiate several classes, and requires to directly use operations with setParamxxx() methods. Moreover, in this way, the cogitant::Environment::projections() method always uses the "standard" projection. This is why it is preferable to "better" integrate the projection variant to the library, and to comply with the usual usage for the projection search. Indeed, usually, one do not instantiate cogitant::OpeProjection, because cogitant::Environment::projections() should be use to calculate projections. In fact, this method uses an instance of cogitant::OpeProjection (as well as all other classes above). Thus, it is this instance that must be modified so that a new operation is taken into account by cogitant::Environment::projections(), as shown in the example below.
Example.The program below redefines the compatibility operation between concept vertices (and only concept vertices): for each couple of 2 labels of concept vertices, these 2 labels are compatible. In other words, a concept vertex can have as an image any concept vertex. In the example, a subclass of cogitant::OpeGraphObjectCompatibility is thus defined, and associated with the operation of searching environment projections.
Example. The program below redefines the filter operation of lists of possible images to filter certain projections. Actually, this example only forbids the 45th update of a list of possible images (and force thus the backtrack at this time), so that some of the projections (among the 360 that are normally found between these 2 graphs) are never reached.
Example. The program below redefines the compatibility between concept vertices and uses this new compatibility operation in order to calculate a max join between two graphs.
Input/output operations do not inevitably act on files, but can take as parameters C++ streams, i.e. instances of subclasses std::istream
and std::ostream
. By the way, most methods are overloaded in order to receive either a filename (in the form of a string) or a stream (input or output, depending on whether it is an input or an output operation).
A classic use of this possibility is to write on the screen a support or graphs for the purpose of debugging or quick viewing of a result. To get this result, simply call the cogitant::Environment::writeGraph() or cogitant::Environment::writeSupport() methods which take as a parameter a std::ostream
and pass them std::court
. This possibility is used in several examples of this documentation.
Note that the format detection being performed by the cogitant::IOHandler from the extension of the filename, it cannot be done from the stream. That is why methods taking a stream as a parameter also take a file format as a parameter.
Classes std::istringstream
and std::ostringstream
enable to consider a buffer in memory as a stream, and, of course input/output operations of Cogitant can be used on such streams. These streams enable, in addition, to access their content as a string, which enables many uses, as a series of bytes can easily be sent to another tool. This function is used in the example below to store and read support and graph into a PostgreSQL database.
Example. The program below uses a PostgreSQL database (http://www.postgresql.org) for storing Cogitant objects (a support and a graph in the example) in the CoGXML format into a BLOB (called BYTEA
in PostgreSQL). More specifically, a table (CogitantStreams
) is created, with two columns: id
contains an object identifier enabling to regain it and cogxml
is a series of bytes containing the CoGXML serialization of the object. Various operations are done by the program to demonstrate the use of the table.
Note that this program requires the libpqxx (http://thaiopensource.org/development/libpqxx/), a C++ API for the DBMS PostgreSQL. If you want to compile this program, don't forget to link your executable to the library in addition to the Cogitant library (a -lpqxx
passed to the linker is enough under Unix).
Even if different operations of reading and writing are done, it's the same principle that is done each time:
std::ostringstream
is declared, the CoGXML output is sent to this stream, then, in order to send the CoGXML code in the database, the method std::ostrinstream::str()
is called and returns a (long) string that contains the full CoGXML code. This string is then sent to the database with a call to the library libpqxx. std::istringstream
is built from this string, and this stream is past to Cogitant loading methods. Of course, the same mechanism can be used to make input/output in a DBMS other than PostgreSQL (or with another API of PostgreSQL) or with other tools that can take their input and ouput in strings.
(This section will be soon completed)