In this section, the general architecture of the library is described. Items being specifically addressed concern the cogitant::Environment class, which is the first class to instantiate when using the library. Indeed, instances of this class offer a centralized management of the support, of conceptual graphs and rules, of input/output and of operations on objects. Then the cogitant::CogitantObject class is described, this class is the heritage root of most classes of the library. So features offered by this class (and especially the properties management) are found in most Cogitant's classes. Finally, in many cases, you have to handle collections of objects (set of vertices, set of types of concepts, set of graphs, and so on.). Classes enabling a management of these collections (containers classes) are described in the last paragraph of this section.
Cogitant provides a object-oriented implementation of the conceptual graphs model [Chein and Mugnier, 1992] [Mugnier and Chein, 1996]. To each model object corresonds a class that offers methods corresponding to basic operations on those objects (cogitant::Support, cogitant::ConceptType, cogitant::Graph, cogitant::Rule, cogitant::Concept, cogitant::Nesting, cogitant::Edge, cogitant::Projection, cogitant::CoreferenceClass, etc.). Data structures used to represent objects are a basic implementation of the model definitions. In this way, a user who knows the model can quickly understand how objects are implemented and develop extensions to classes supplied with Cogitant.
In order to provide more flexibility, complex operations of the model (projection, rule application, etc.) are not directly accessible as methods of the cogitant::Graph class but are available from specific classes (subclasses of cogitant::Operation), which allows you to redefine processes performed to calculate these operations, without redefining classes that offer a graph or rule representation.
All classes are redefining the
<< operator on a
ostream, which enables a fast display of objects. However the display produced in this way cannot be used to save objects in files, because no reading mechanism in this format is supplied. By contrast, input/ouput operations are supplied in formats BCGCT, CoGXML, CGIF, and linear form.
The class cogitant::Environment enables the management of support, graphs and rules defined on this support, an easier access to operations using supports, graphs and rules, and in particular input/output operations and operations defined in the model of conceptual graphs. You can do without this class, but the use of an environment has many advantages:
In this way, it is impossible to attempt incorrect operations involving graphs defined on different supports or graphs defined on a given support and another support.
More specifically, the support is accessible via the method cogitant::Environment::support() that returns a reference to the support related to the environment.
In this way, environment destruction causes graphs destruction, and you don't have to take care about unallocating these graphs one by one.
In addition, the Environment class being provided with an allocating operator and a copy constructor which copy graphs and rules, it is very easy to work on a copy of the graphs.
Number of features are available to browse or search graphs and rules contained in the environment.
Objects stored in the environment can be conceptual graphs (cogitant::Graph), or rules (cogitant::Rule). In order to handle these two types of objects in a unified way, but also because these two types of objects share common features, these two classes derive from cogitant::EnvironmentObject. In this way, objects contained in the environment are handled as pointers on cogitant::EnvironmentObject objects. This set of objects can be accessed by the method cogitant::Environment::objects().
To each object stored in the environment is associated a unique identifier (cogitant::iSet). This identifier, allocated at the object creation, enables to access it in constant time. Thus, the method cogitant::Environment::objects(iSet) returns the object corresponding to the identifier passed as parameter. Nevertheless, this method returns a pointer on a cogitant::EnvironmentObject, so you cannot call directly specific methods of cogitant::Graph or cogitant::Rule on the value returned. Of course you can use a conversion of type if the user is sure that the object in question is either a graph or a rule, but you should rather use conversion methods cogitant::EnvironmentObject::asGraph() and cogitant::EnvironmentObject::asRule() which cause an exception in case of an invalid conversion (see Error management). There is, however, an easier way to access directly objects as graphs or rules: methods cogitant::Environment::graphs(iSet) and cogitant::Environment::rules(iSet) return, respectively, a pointer on a graph and a pointer on a rule. Obviously, if the passed identifier do not correspond to a graph (or a rule), an exception is thrown.
Creating new graphs or rules in the environment is done by calling methods cogitant::Environment::newGraph() and cogitant::Environment::newRule() who create empty objects and return the identifier assigned to the created object. You can also create a graph (resp. a rule) from an existing graph (resp. a rule) by calling the method cogitant::Environment::newGraph(iSet,bool) (resp. cogitant::Environment::newRule(iSet)) which creates a copy of the object of which the identifier is passed as a parameter and returns the identifier of the created object.
In some cases, you should avoid to save manipulated graphs in the environment memory, particularly in the case of temporary graphs used in a calculation. For that, the method cogitant::Environment::newExternalGraph() returns a pointer on an empty graph created by the call to this method. Obviously, such a graph must be explicitly destroyed (by
delete) as it is not bound to the environment. However, it is forbidden to destroy by
delete graphs or rules contained in the environment: to destroy such objects, you have to call the method cogitant::Environment::deleteObject(iSet), cogitant::Environment::deleteGraph(iSet) or cogitant::Environment::deleteRule(iSet). Finally, methods cogitant::Environment::addObject(EnvironmentObject*) and cogitant::Environment::detachObject(iSet) let you add, respectively, to the environment an object which had previously been created as external to an environment and to get out of the environment an object that has been created inside it.
Example. Creating an environment, adding a type of concept to the support, creating a graph made of a concept vertex, copying this graph in the environment. The copy is then extracted from the environment, to be manipulated as a dynamic object, which must be then explicitly destroyed.
A number of supplied methods enable reading and writing supports, graphs and rules in different formats. The used format is selected according to the extension of the file read or written.
Inputs and outputs are managed through an instance of cogitant::IOHandler, included in the environment. However, as part of a classic use of the library, the user does not have to take care about this class, and can directly use environment methods which provide input/output features. Note that the use of the class IOHandler is required to add read / write features in new formats.
Reading a support from a file is done by the method cogitant::Environment::readSupport() to which is passed a string containing the name of the file to load. This file can be interpreted in different ways according to its extension: By default CoGITaNT provides a reading of supports in formats BCGCT (extension
bcs), CoGXML (extension
cogxml). If the extension is not recognized, the file is interpreted as a BCGCT format. If the file does not comply with the format or contain data that do not comply with the model of conceptual graphs, an exception is thrown (see Error management).
Reading graphs or rules is done by the method cogitant::Environment::readGraphs(). This method takes as a parameter a string containing the name of the file to load. A second optional parameter is a pointer to a
vector<iSet>: if a non
NULL pointer is passed, this vector will contain, after the call, the object identifiers that have been loaded from the chosen file. In the case where the loaded file contains only one graph, it's easier to use the return value of this method, which is the identifier of the first graph loaded from the file (see the example below). By default, the environment provides features for reading graphs in formats BCGCT, CoGXML and CGIF (2001, Core, Extended, extension
Support writing is done through the call of the method cogitant::Environment::writeSupport(), which accepts a parameter, a string containing the name of the file to generate. If this file exists, it is overwritten by the support. Default formats for support writing are THC, BCGCT, CoGXML and CGIF (2001 and extended).
Writing graphs (and rules) is done by the call to methods cogitant::Environment::writeGraphs() and cogitant::Environment::writeGraph(). Both methods take as the first parameter a filename, and as the second parameter, respectively a
vector<iSet> containing identifiers of the objects to be saved, and a iSet corresponding to the identifier of the object to be saved. By default, the backup can be done in the BCGCT format, in the CoGXML format (
cogxml extension), in the CGIF format (2001, Core, Extended) or in the linear form (
lf extension). Warning, only formats BCGCT and CoGXML let you store all the properties associated with objects handled in memory: properties are not saved in CGIF and linear form.
Example. The example below illustrates the loading of a support and a graph in format BCGCT, the displaying of the graph loaded on the screen, and the backup of this graph in the linear format .
To display graphs on the screen in order to control that these graphs are in accordance with what one think to have built, you can call the output operator of the cogitant::Graph class, which displays the "internal form" of the graph, which is indeed accurate as identifiers of vertices and the internal structure of the object are represented, but which is not always easy to interpret. That's why the method cogitant::Environment::writeGraph(ostream&,iSetIOHandler::Format) can be preferred to the output operator. This method takes as a parameter an output stream (usually
cout), a graph identifier, and a file format among those known by the platform. If this format is not specified, the output is carried out in linear format which is often the easiest one to interpret in the case of small graphs (not nested), but you can specify the output format, by passing, for example, cogitant::IOHandler::BCGCT as a parameter.
Example. In the example below, a support and a graph are loaded, then the graph is displayed in internal form, linear form, BCGCT and CoGXML.
Operations of the conceptual graph model (projection, rule application, etc.) are available through subclasses of cogitant::Operation. For further details regarding the operations, and how to define new operations, or modify existing operations, see section Operations. However, in order to provide a simplified use mode, the main operations are also available from methods of the environment class.
So, the method cogitant::Environment::projections(iSet,iSetResultOpeProjection&) calculates projections between two graphs marked by their identifiers (projections from the first into the second). The result is stored in the passed cogitant::ResultOpeProjection instance. The use of this class (rather than a basic set of cogitant::Projection) for the storage of projections calculation enables to configure the type of search: by calling methods cogitant::ResultOpeProjection::memoProjections(bool), cogitant::ResultOpeProjection::maxSize(nSet) before the call to cogitant::Environment::projections(), you can choose whether only the existence of projections must be searched, if their number must be determined, or, finally, if the projections themselves must be calculated and stored. See also Projection calculation.
Example. The program below loads two graphs and calculates projections from the first into the second. The number of projections found is displayed.
Other operations can be calculated from the environment:
These methods take as parameters pointers on graphs or rules, in this way, you can use both on objects in the environment as well as on objects out of the environment (but defined in the same support). Thus, for objects in the environment, you have to call cogitant::Environment::graphs(iSet) or cogitant::Environment::rules(iSet) to get a pointer on objects from their identifiers.
Most classes of the library derive from cogitant::CogitantObject. This abstract class provides a number of features whose main one is a management of the properties associated with objects.
To each object is indeed associated with a set of properties (cogitant::PropertySet) which is a set of couples (type of the property, value). Sets of properties enable, among other things, to store the type of a concept vertex, the label of a nesting type, the signature of a relation type, or any other information as properties are restricted to predefined values. The set of properties associated with an object is available through the method cogitant::CogitantObject::properties(). From a set reference, you can add properties (cogitant::PropertySet::set()), search a property by its type (cogitant::PropertySet::read(), cogitant::PropertySet::get()) or remove a property (cogitant::PropertySet::remove()).
More precisely, a set of properties is made up of several instances of cogitant::Property, each of which has a member called type (cogitant::Property::type()), this variable is a Property::Type which identifies, for example, the fact that this is a graph name (cogitant::Property::GRAPH_NAME) or the position of an object (cogitant::Property::DISPLAY_POS_X). Obviously, a property also associates a value to this type, and the variable representing this value can be of different types (cogitant::Property::valueType()):
vector<iSet> . The value itself is accessible by cogitant::Property::getInt(), cogitant::Property::getPtr(), cogitant::Property::getString() or cogitant::Property::getVectorISet(). However, as part of a classic use of properties sets, you don't have to directly use methods of the cogitant::Property class, but only methods of cogitant::PropertySet.
Example. The program below creates a cogitant::Environment (which is a subclass of cogitant::CogitantObject), and it attributes a property of type cogitant::Property::COMMENT of value "ENV" to it. This property is then searched and displayed before being removed. Finally, a property matching no predefined type is added to the object. Such properties are identified by a string.
The other feature offered by cogitant::CogitantObject is useful for programs development: these are two methods respectively returning the object class name (cogitant::CogitantObject::className()) and a displayable form of the object (cogitant::CogitantObject::toString()). The class name is returned thanks to the features RTTI (run-time type information) of C++, and so you don't have to redefine this method in subclasses. The toString method, which returns by default the class name can be redefined to return a textual representation of the object in question. In this way, all objects having this method, it is easy to display object features for the program development. In addition, platform classes are redefining the operator of output on a stream that calls the toString method, what can very easily display the string associated with each object.
Example. The program below creates a cogitant::Environment, and adds a concept type "Person" to all concept types. A graph comprising a single concept vertex, of the "Person" type, is then added to the environment. The environment and the graph are then displayed.
Running this programm returns the following output (which varies depending on the compiler used, because the RTTI is not defined in the same way in all compilers, in particular regarding the the inclusion of namespace in the class name):
To interpret this display, you have to know the structure of the support and the graphs (described later), but one may observe that the string associated with the environment contains a description of the support and that this description lists the included sets. One can specifically note that the set of concept types contains a type, with
0 as identifier and
Person as label. As for the graph, it is made up of nodes and edges, and in particular of two nodes, the first, having an identifier of
0, is a cogitant::InternalGraph and the second, having an identifier of
1 is a cogitant::Concept, the following
0 is the identifier of the concept vertex type: here this is concept type
In the conceptual graphs model, many objects are defined from sets. Thus, a basic conceptual graph is made of a set of concept vertices and a set of relation nodes, but also of a set of edges. The support is, as for it, made of a set of concept types, of a set of individual markers, etc. To provide a model implementation pleasant to use, you have then to define a class "set" which can be used in different occasions. Contrary to a popular language which only enables to define containers of
Object and thus imposes an abuse of explicit conversions, C++ enables the definition of really generic classes. It is this option which was adopted in the cogitant::Set class, which is a generic class (pronounce "<em>template</em>") whose use is inspired from classes of the std library.
The cogitant::Set class is actually an abstract class that provides an interface to the data structure "set". Indeed, as the concept of set can be used in various occasions, it is difficult to define an implementation of a class which is perfectly suited to all uses: we preferred defining an abstract class, whose data structures and methods can be concretised in different ways. Of course, a concrete class derived from cogitant::Set comes with the library, it is the cogitant::Set_Simple class which provides a basic set implementation. In this class, a set of elements is composed of a
vector of elements. The drawback of this implementation is the required resizing of the data structure when many additions are done (such resizings are nevertheless done in a transparent manner to the user of the class, can be limited by the call to cogitant::Set::reserve(), and are done "per unit": the vector is not resized at each addition). Nevertheless, such an implementation has many advantages: the required memory space is significantly lower than list one, and the effective processing times are very satisfactory (because most operations do not use dynamic allocation, while it is the case for adding or removing an item to a list).
The elements of a set are all marked with a unique identifier of type cogitant::iSet (this is the same type as the one used to identify graphs and rules contained in an environment, simply because an environment contains a Set<EnvironmentObject>). The identifier associated with an element can be obtained by the method cogitant::Set::iAdd() which takes as a parameter an object, adds it to the set and returns the identifier associated with that object. Of course, the class being defined by a
template, the compiler checks the validity of the method calls: incorrect operations such as adding a type of concept to a graph cause a compiling error. From an identifier, you can access to the element (cogitant:Set::iGetContent() or by using cogitant::Set::operator(), see example below) in read and write mode, and delete the item of the set (cogitant::Set::iDelete()).
Example. To explicitly instantiate a set, you cannot use the Set class (abstract), you have to choose the corresponding concrete class (cogitant::Set_Simple, think to the corresponding header file). This example illustrates the use of a set of pointers to types of concepts: a first type is dynamically created and added to the set, its identifier is stored in
i (displayed), a second type is added, but its identifier is not memorized. Nevertheless, this identifier is recovered by the call to cogitant::Set::iFind(). Finally, the last line illustrate an access to an element from its identifier.
A feature usually very much used on the data structures is the traversal. With the Set class, there are two ways to traverse the elements: the first uses identifiers (cogitant::iSet), and the second uses iterators, whose use is very close to std iterators. A traversal with identifiers is done with methods cogitant::Set::iBegin() which return the identifier of the first element of the set, cogitant::Set::iNext() which increment the identifier passed as a parameter and cogitant::Set::iEnd() which return an incorrect identifier (corresponding to the element "following" the last).
Example. Loading of a support and display of the label of all types of loaded concepts.
Using iterators with the cogitant::Set class comes close to data structures of the std library, and also enables a selective traveral of certain elements (only the concepts vertices among all the graph elements, for example), which is detailed in the following sections. For a traverasl with iterators, you have to use cogitant::Set::begin() and cogitant::Set::end() methods. The iteration is done by the operator
++ of the iterator (cogitant::SetIterator::operator++()) and the access to the current element is done through the use of the dereferencing operator (cogitant::SetIterator::operator*()) of the iterator, like with the std library.
Example. Support loading and display of the label of all types of loaded concepts.