Cette section décrit l'utilisation des opérations fournies dans la bibliothèque. Certaines de ces opérations (projection, entrées/sorties) ont déjà été vues dans les sections précédentes, sous la forme de méthodes de la classe cogitant::Environment. Mais ces méthodes ne sont que des raccourcis pour l'utilisation de cogitant::Operation (et ses sous-classes), c'est pourquoi, dans le cadre d'une utilisation avancée de la bibliothèque, il est préférable de connaître l'utilisation de cette classe pour bien comprendre le fonctionnement des méthodes correspondant aux opérations définies sur le modèle, les fonctionnalités d'entrées/sorties ainsi que pour définir de nouvelles opérations. Dans cette partie, nous détaillons les principales opérations de la bibliothèque, et décrivons la façon d'utiliser ces opérations en tant qu'instances de sous-classes de cogitant::Operation et à travers les méthodes "raccourcis" de cogitant::Environment.
L'approche choisie pour offrir des opérations dans la bibliothèque consiste donc à utiliser des objets instances de cogitant::Operation (ou plutôt de ses sous-classes, car il s'agit d'une classe abstraite). Cette solution a été préférée à la définition de méthodes dans la classe cogitant::Graph ou cogitant::Rule car elle offre plus de flexibilité. En effet, que ce soit pour définir de nouvelles opérations (par exemple la gestion d'un nouveau format de fichier) ou pour fournir une nouvelle implantation d'opérations existantes (par exemple un algorithme de projection plus efficace), la solution consistant à utiliser des méthodes de cogitant::Graph demande de définir une nouvelle sous classe, dans laquelle la méthode correspondant à l'opération est redéfinie. Ceci pose évidemment le problème de la définition de plusieurs nouvelles opérations : dans ce cas, comment organiser la relation d'héritage sur la (les) nouvelle(s) classe(s). Au contraire, la solution retenue permet de définir de nouvelles classes d'opérations qui recoivent comme paramètres les objets du modèle. Ainsi, il est très simple de définir de nouvelles implantations d'opérations existantes ou de nouvelles opérations. De plus, la bibliothèque manipulant des références sur des instances de (sous classes de) cogitant::Operation, il est très simple d'incorporer ces nouvelles opérations à la bibliothèque, et d'utiliser des méthodes de la bibliothèque qui appeleront de façon totalement transparente les nouvelles opérations.
De façon générale, les opérations s'utilisent en instanciant un objet d'une sous-classe de cogitant::Operation, en appelant les méthodes setParamxxx() pour fixer les différents paramètres de l'opération, puis en appelant la méthode run() qui effectue le calcul. Enfin, les méthodes getResultxxx() permettent de récupérer le résultat.
Les graphes conceptuels manipulés dans Cogitant peuvent être non connexes. Cependant la structuration d'un graphe en composantes connexes peut être utile, c'est pourquoi plusieurs méthodes de la bibliothèque permettent de déterminer si un graphe est connexe, quel est le nombre de composantes connexes d'un graphe et quelles sont ces composantes connexes (les sommets contenus dans chaque composante). Ces différentes informations peuvent être calculées en appelant les méthodes cogitant::Environment::isConnected() (connexité du graphe), cogitant::Environment::connectedComponentsSize() (nombre de composantes connexes) et cogitant::Environment::connectedComponents() (composantes connexes). Toutes ces méthodes prennent comme paramètre un (pointeur sur un) cogitant::Graph et un cogitant::iSet repérant l'identificateur du cogitant::InternalGraph dont la connexité doit être déterminée (par défaut 0
, c'est à dire le graphe de niveau 0). En effet, dans le cas de graphes emboîtés, un cogitant::Graph contient plusieurs "graphes", et s'il n'y a aucun intérêt à déterminer le nombre de composantes connexes de tous ces graphes, il peut être intéressant de déterminer cette information sur l'un d'entre eux (et pas seulement sur le graphe de niveau 0).
La somme disjointe est accessible simplement à partir de la méthode cogitant::Environment::disjointSum() qui prend comme paramètres deux (pointeurs sur des) cogitant::Graph et qui modifie le premier pour lui rajouter le second (qui n'est pas modifié). Cette méthode utilise en fait la classe cogitant::OpeDisjointSum, dont la documentation décrit les différentes fonctionnalités offertes par cette classe qui permet d'effectuer des opérations plus complexes que la simple somme disjointe (cette opération est notamment utilisée lors de chargement de fichiers quand une référence à un graphe déjà existant est faite dans un emboîtement (le graphe doit alors être copié comme graphe emboîté, et les classes de coréférences doivent être éventuellement mises à jour) et lors de l'application d'une règle selon une projection (ajout de la conclusion et fusion de sommets, voir plus bas)).
La recherche de projections d'un graphe dans un autre se fait à l'aide de la méthode cogitant::Environment::projections(), à qui on doit passer le graphe projeté et le graphe dans lequel on projette. Cette méthode est en fait surchargée et peut prendre comme paramètres des identificateurs de graphes (cogitant::iSet identifiant le graphe dans l'environnement) ou des pointeurs sur des cogitant::Graph. Le troisième paramètre de cette méthode, un cogitant::ResultOpeProjection passé par référence, contiendra après l'appel le résultat de l'exécution de la méthode. Mais cet objet permet aussi de configurer la recherche. En effet, selon les usages, on peut être intéressé par différentes recherches, qui seront plus ou moins couteuses en temps et en mémoire. On peut distinguer principalement 3 usages, qui sont présentés ici du moins couteux au plus couteux, et illustrés plus bas par un exemple.
false
comme paramètre (pour signaler que les projections ne doivent pas être mémorisées), et cogitant::ResultOpeProjection::maxSize(cogitant::nSet) avec comme paramètre 1
(pour signaler que la recherche doit être interrompue dès qu'une projection est trouvée). Ces deux méthodes doivent être appelées sur le cogitant::ResultOpeProjection avant l'appel à cogitant::Environment::projections(). Une fois que cette méthode a été appelée, il est nécessaire d'appeler cogitant::ResultOpeProjection::isEmpty() pour savoir si une projection a été trouvée. false
comme paramètre avant l'appel à cogitant::Environment::projections(). Après l'appel, la méthode cogitant::ResultOpeProjection::size() permet de connaitre le nombre de projections trouvées. Exemple. L'exemple ci-dessous illustre les trois cas présentés ci-dessus. L'affichage des projections dans le troisième cas fait appel à l'opérateur de sortie de cogitant::Projection qui affiche des couples d'identificateurs de sommets dans les graphes : le premier élément de chaque couple est l'identificateur (iSet) d'un cogitant::GraphObject du graphe projeté et le second élément est l'identificateur de son image.
Itérateur de projections
A l'aide des classes cogitant::OpeProjectionBundle et cogitant::ProjectionIterator, les projections ne sont pas calculées directement par un simple appel (comme c'est le cas avec cogitant::Environment::projections()) : Après une initialisation par un appel à cogitant::OpeProjectionBundle::begin(), un cogitant::ProjectionIterator calcule la cogitant::Projection suivante à chaque fois que son opérateur ++ est appelé. Notez que la modification des deux graphs alors que les projections sont en cours de calcul est strictement interdite. Notez aussi qu'un cogitant::OpeProjectionBundle ne peut calculer, à un moment donné, seulement les projections entre deux graphes, même si deux cogitant::ProjectionIterator sont déclarés. Donc, afin de calculer simultanément les projections entre plusieurs couples de graphes, plusieurs cogitant::OpeProjectionBundle sont nécessaires, et ils peuvent être obtenus par cogitant::Environment::newOpeProjectionBundle().
Exemple.
Modifications de l'opération de recherche de projections
Comme toutes les autres méthodes de cogitant::Environment qui donnent accès à des opérations, cogitant::Environment::projections() n'est qu'un raccourci qui utilise des sous-classes de cogitant::Operation. Plus précisément, c'est la classe cogitant::OpeProjection qui se charge du calcul des projections. Cette opération recherche les projections par un algorithme de backtrack qui calcule dans un premier temps les listes d'images possibles (de chaque cogitant::GraphObject du graphe projeté dans les cogitant::GraphObject du graphe dans lequel on cherche les projections), puis filtre ces listes. Le filtre se fait en choisissant un sommet o1 du graphe projeté, et une de ses images o2 parmi sa liste d'images possibles. En choisissant ce couple comme faisant partie de la projection, cela induit des contraintes sur les voisins de o1 : si il existe une arête étiquetée i entre o1 et o3, alors o3 ne peut avoir comme images que des sommets o4 tels qu'il existe une arête étiquetée i entre o2 et o4. La liste des images possibles de o3 peut donc être filtrée.
L'opération cogitant::OpeProjection ne définit en fait que le schéma principal de recherche des projections. La méthode run() de cette méthode fait en effet appel à d'autres opérations, spécialisées dans une tâche. Ainsi, pour modifier l'opération de recherche de projections, il "suffit" d'écrire une sous-classe de cogitant::OpeProjection (au pire) ou une sous-classe d'une des opérations suivantes pour modifier une partie du comportement de la recherche :
La "personnalisation" du calcul des projections peut donc passer par l'écriture d'une sous classe de l'une ou plusieurs des classes présentées ci-dessus. Évidemment, dans ce cas, il est nécessaire de préciser à cogitant::OpeProjection que cette nouvelle classe doit être utilisée. Pour cela, il est nécessaire d'instancier la nouvelle classe et de passer une instance à l'instance de cogitant::OpeProjection utilisée pour lui signaler d'utiliser la nouvelle opération. Mais il faut pour cela avoir accès à une instance de cogitant::OpeProjection. Il faut donc instancier cette classe et lui fournir des instances de toutes les opérations annexes, qu'il s'agisse des classes "standard" ou de nouvelles sous-classes. Cette façon de faire n'est pas très agréable à utiliser car elle demande d'instancier plusieurs classes, et demande d'utiliser directement les opérations avec les méthodes setParamxxx(). De plus, de cette façon, la méthode cogitant::Environment::projections() utilise toujours la projection "standard". C'est pourquoi, il est préférable de "mieux" intégrer la variante de la projection à la bibliothèque, et se conformer à l'usage habituel pour la recherche de projections. En effet, habituellement, on n'instancie pas cogitant::OpeProjection, car on utilise cogitant::Environment::projections() pour calculer les projections. En fait, cette méthode utilise une instance de cogitant::OpeProjection (ainsi que toutes les autres classes ci-dessus). C'est cette instance qu'il faut donc modifier pour qu'une nouvelle opération soit prise en compte par cogitant::Environment::projections(), comme l'illustre l'exemple ci-dessous.
Exemple. Le programme ci-dessous redéfinit l'opération de compatibilité entre sommets concepts (et uniquement sommets concepts) : quelles que soient 2 étiquettes de sommets concepts, ces 2 étiquettes sont compatibles. En d'autres termes un sommet concept peut avoir comme image tout sommet concept. Dans l'exemple, une sous classe de cogitant::OpeGraphObjectCompatibility est donc définie, et associée à l'opération de recherche des projections de l'environnement.
Exemple. Le programme ci-dessous redéfinit l'opération de filtre de listes d'images possibles pour filtrer certaines projections. En fait, cet exemple ne fait qu'interdire la 45ème mise à jour d'une liste d'image possibles (et force donc le backtrack à ce moment là), ce qui fait que certaines des projections (parmi les 360 qui sont normalement trouvées entre ces 2 graphes) ne sont jamais atteintes.
Exemple. Le programme ci-dessous redéfinit l'opération de compatibilité entre deux sommets concepts et utilise cette nouvelle compatibilité pour calculer un joint max entre deux graphes.
Les opérations d'entrées/sorties n'agissent pas obligatoirement sur des fichiers, mais peuvent prendre comme paramètres des flux C++, c'est-à-dire des instances de sous-classes std::istream
et std::ostream
. La plupart des méthodes sont d'ailleurs surchargées afin de recevoir au choix un nom de fichier (sous la forme d'une chaîne de caractères) ou un flux (de sortie ou d'entrée, selon s'il s'agit d'une opération de sortie ou d'entrée).
Une utilisation classique de cette possibilité est l'écriture à l'écran d'un support ou de graphes à des fins de débogage ou visualisation rapide d'un résultat. Pour obtenir ce résultat, il suffit d'appeler les méthodes cogitant::Environment::writeGraph() ou cogitant::Environment::writeSupport() prenant comme paramètre un std::ostream
et leur passer std::cout
. Cette possiblité est utilisée dans plusieurs exemples de cette documentation.
Il est à noter que la détection de format s'effectuant par le cogitant::IOHandler à partir de l'extension du nom de fichier, elle devient impossible à partir des flux. C'est pourquoi les méthodes prenant comme paramètre un flux prennent aussi comme paramètre un format de fichier.
Les classes std::istringstream
et std::ostringstream
permettent de considérer un buffer en mémoire comme un flux, et, évidemment les opérations d'entrées/sorties de Cogitant peuvent être utilisées sur de tels flux. Ces flux permettent, de plus, d'accéder à leur contenu comme une chaîne de caractères, ce qui permet de nombreux usages, car une suite d'octets peut facilement être transmise à un autre outil. Cette fonction est utilisée dans l'exemple ci-dessous pour stocker et lire support et graphe dans une base de données PostgreSQL.
Exemple. Le programme ci-dessous utilise une base de données PostgreSQL (http://www.postgresql.org) pour stocker des objets Cogitant (un support et un graphe dans l'exemple) au format CoGXML dans un BLOB (appelé BYTEA
dans PostgreSQL). Plus précisément, une table (CogitantStreams
) est créée, avec deux colonnes : id
contient un identifiant d'objet permettant de le retrouver et cogxml
est une suite d'octets contenant la sérialisation CoGXML de l'objet. Diverses opérations sont effectées par le programme pour démontrer l'utilisation de la table.
A noter que ce programme nécessite la libpqxx (http://thaiopensource.org/development/libpqxx/), une API C++ pour le SGBD PostgreSQL. Si vous désirez compiler ce programme, il ne faut donc pas oublier de lier votre exécutable à cette bibliothèque en plus de la bibliothèque Cogitant (un -lpqxx
passé à l'éditeur de liens suffit sous Unix).
Même si différentes opérations de lecture et d'écriture sont effectuées, c'est le même principe qui est mis en oeuvre à chaque fois :
std::ostringstream
est déclaré, la sortie CoGXML est envoyée sur ce flux, puis, afin d'envoyer ce code CoGXML dans la base, la méthode std::ostrinstream::str()
est appelée et retourne une (longue) chaîne de caractères qui contient la totalité du code CoGXML. Cette chaîne de caractères est ensuite envoyée dans la base à l'aide d'un appel à la bibliothèque libpqxx. std::istringstream
est construit à partir de cette chaîne, et ce flux est passé aux méthodes de chargement de Cogitant. Bien évidemment, le même mécanisme peut être utilisé pour effectuer des entrées/sorties dans un SGBD autre que PostgreSQL (ou avec une autre API de PostgreSQL) ou d'autres outils qui peuvent prendre leurs entrées et sorties dans des chaînes de caractères.
(Cette section sera complétée prochainement)