A la fin de ce notebook, vous saurez : 1. Comprendre le rôle des raisonneurs OWL dans le Web Sémantique 2. Comparer différents raisonneurs (owlrl, HermiT, reasonable, Growl) 3. Mesurer les performances d’inférence sur une ontologie de test 4. Choisir le raisonneur approprié selon vos besoins
Concepts clés
Concept
Description
Raisonneur OWL
Moteur d’inférence qui déduit des connaissances implicites à partir d’une ontologie
OWL 2 RL
Profil OWL polynomial, implémentable avec des règles Datalog
Processus de dérivation de nouveaux triplets à partir de triplets existants
Prérequis
Notebook SW-7-OWL recommandé (pour comprendre les bases d’OWL)
Python 3.10+ avec pip
Durée estimée : 45 minutes
Introduction
Les raisonneurs OWL (OWL reasoners) sont des moteurs d’inférence capables de déduire des connaissances implicites à partir d’une ontologie. Ils implémentent les règles de sémantique formelle d’OWL (Web Ontology Language).
Les profils OWL 2 (DL, RL, EL, QL) normalisent un compromis expressivité/complexité calculatoire. Ils sont définis dans le document OWL 2 Profiles (Motik, Grau, Horrocks et al., Recommandation W3C 2009/2012), lui-même ancré dans la sémantique formelle d’OWL 2 (Motik, Patel-Schneider, Cuenca Grau, W3C Rec 2012) et la théorie des logiques de description (Baader, Calvanese, McGuinness, Nardi, Patel-Schneider (eds.), The Description Logic Handbook, Cambridge University Press, 2e éd. 2007).
Rappels sur les profils OWL 2
Profil
Complexité
Usage typique
Raisonneurs
OWL 2 DL
Exp-temps complet
Médecine, ontology lourde
HermiT, FaCT++
OWL 2 RL
Polynomial (P)
Règles, RDF Schema
owlrl, reasonable, Growl
OWL 2 EL
Polynomial (P)
Très large ontologie
ELK, Konclude
OWL 2 QL
Polynomial (P)
Query rewriting (OWL API)
Quest
Raisonneurs comparés
Raisonneur
Langage
OWL Profile
Intégration
owlrl
Python
OWL 2 RL
RDFLib native
OWLReady2 + HermiT
Python/Java
OWL 2 DL
pip + Java bridge
reasonable
Rust + Python
OWL 2 RL
pip + Rust compiled
Growl
C (+ Z3 verified)
OWL 2 RL (76/78 règles)
CLI/subprocess
Configuration
Installons les dépendances nécessaires.
Deux familles de dépendances sont attendues : les raisonneurs OWL 2 RL (owlrl en Python pur, reasonable en Rust avec bindings Python) et la pile OWL 2 DL (owlready2, qui embarque un pont vers la JVM et le raisonneur HermiT), plus les outils de visualisation (matplotlib, pandas). La cellule suit le patron « installer seulement si l’import échoue » : un environnement déjà provisionné n’est pas modifié, et la confirmation unique de fin de cellule vaut pour l’ensemble des six packages.
# Installation des packages Pythonimport sysimport subprocessdef install(package): subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", package])# Installer si nécessairepackages = ["rdflib", "owlrl", "owlready2", "reasonable", "matplotlib", "pandas"]for pkg in packages:try:__import__(pkg)exceptImportError:print(f"Installation de {pkg}...") install(pkg)print("✓ Tous les packages sont installés")
✓ Tous les packages sont installés
Importation des bibliotheques principales : rdflib, owlrl, OWLReady2 et reasonable.
L’importation sert aussi de point de controle d’environnement : la cellule affiche les versions effectivement chargees (interpreteur, RDFLib, owlrl) plutot que de supposer celle du contexte d’execution. C’est une habitude a conserver pour tout travail comparatif — les corpus de regles OWL evoluent entre versions d’un raisonneur, et un ecart de comportement observe d’une machine a l’autre se diagnostique d’abord en comparant ces lignes. Les avertissements sont filtres pour ne pas polluer les sorties comparatives.
Chaque import couvre un role distinct dans la suite : rdflib porte le graphe de triplets et sa serialisation Turtle, owlrl y applique la fermeture de regles OWL 2 RL, owlready2 expose l’ontologie comme classes Python et delegue le raisonnement a HermiT (JVM), tandis que reasonable fournit la meme fermeture RL via des bindings Rust (PyO3). Les imports tolerants (try/except) des sections owlready2, reasonable et Growl garantissent que le notebook reste executable si l’un d’eux manque : chaque section de mesure teste la disponibilite du moteur avant de chronometrer, et le tableau comparatif final n’agrege que les raisonneurs effectivement presents dans l’environnement.
Avant de construire l’ontologie de test et d’interpreter les inférences des raisonneurs, il faut poser les deux hypothèses sémantiques fondatrices d’OWL. Elles distinguent OWL d’une base de données relationnelle (hypothèse du monde fermé + unicité des noms) et conditionnent toutes les inférences que l’on observera.
Hypothèse du monde ouvert (Open World Assumption, OWA). En OWL, l’absence d’une information n’est pas une information d’absence. Une assertion manquante signifie « on ne sait pas », jamais « c’est faux ». Conséquence immédiate : si l’on déclare tomate a Vegetal (cf. l’exemple guidé 4), OWL n’en déduit pas que tomate n’est pas un Animal — sauf si l’on asserte explicitement que Vegetal et Animal sont disjointes (owl:AllDisjoint / owl:disjointWith). C’est précisément ce mécanisme qui rend la détection d’incohérence de l’exemple 4 non arbitraire : HermiT (DL) signale tomate comme contradictoire uniquement parce que la disjointness est assertée ; sans elle, rien ne serait incohérent.
Hypothèse de non-unicité des noms (non-Unique Name Assumption, non-UNA). OWL ne suppose pas que deux URI distinctes désignent des individus distincts. ex:tomate1 et ex:tomate2 peuvent dénoter le même individu tant que rien ne les distingue. Pour forcer deux individus à être différents, il faut l’assertion explicite owl:differentFrom (entre deux individus) ou owl:AllDifferent (sur un ensemble). On retrouvera ce point dans l’exemple guidé 6 (benchmark BigFamily = hasChild min 2) : la cardinalité n’est atteinte que parce que child1 et child2 sont déclarés AllDifferent — sans cela, sous non-UNA, le raisonneur pourrait les confondre en un seul individu et la borne min 2 ne serait pas satisfaite.
Concrètement, sur l’ontologie Pizza de ce notebook. Posons aux raisonneurs la question : « pepperoni est-elle une VegetarianPizza ? ». Le graphe initial de 40 triplets ne contient aucun triplet qui exclut cette appartenance : pepperoni est bien typée NonVegetarianPizza et garnie de ham (un MeatTopping), mais rien n’asserte que NonVegetarianPizza et VegetarianPizza sont disjointes — pas plus que leurs garnitures. Sous OWA, l’absence du triplet pepperoni rdf:type VegetarianPizza ne vaut pas négation : le raisonneur répond « on ne sait pas », jamais « non ». C’est le lien direct avec les triplets RDF manquants : un raisonneur OWL ne consulte que les triplets présents, et tout ce qui n’est pas écrit n’existe pas pour lui.
Un unique triplet suffirait à changer la conclusion : en ajoutant NonVegetarianPizza owl:disjointWith VegetarianPizza, l’appartenance déjà assertée de pepperoni à NonVegetarianPizza interdirait à tout modèle cohérent de la classer aussi VegetarianPizza — un raisonneur DL pourrait alors répondre « non » à la question (cf. la réalisation d’instances de l’exemple 6). On voit déjà pourquoi ce notebook croise quatre raisonneurs : matérialisation positive, détection d’incohérence et réponses négatives sont des services distincts, que tous les profils ne rendent pas.
Pourquoi c’est un prérequis. Sans OWA, la détection d’incohérence de l’exemple 4 paraîtrait arbitraire ; sans non-UNA, l’exemple 6 réussit ou rate son cardinal selon qu’on asserte la différence des individus. Garder ces deux hypothèses en tête est indispensable pour interpréter correctement chacune des inférences qui suivent.
Ontologie de test
Créons une ontologie de test basée sur le domaine de la pizza (inspirée de la Pizza Ontology, un benchmark standard en OWL).
La Pizza Ontology est l’ontologie didactique de référence du Web Sémantique, popularisée par les tutoriels Protégé de l’Université de Manchester (Rector, Stevens et al.). Son usage pédagogique systématique est analysé dans Rector, Drummond, Stevens et al., OWL pizzas: Practical expérience of teaching OWL-DL — common errors, common patterns (EKAW 2004), qui catalogue les difficultés récurrentes des apprenants OWL.
# Namespace pour notre ontologieEX = Namespace("http://example.org/pizza#")# Création du grapheg = Graph()g.bind("ex", EX)g.bind("owl", OWL)g.bind("rdfs", RDFS)# Déclaration de l'ontologieg.add((EX.PizzaOntology, RDF.type, OWL.Ontology))# Classes de baseclasses = [ EX.Pizza, EX.PizzaBase, EX.ThinAndCrispyBase, EX.DeepPanBase, EX.PizzaTopping, EX.VegetarianTopping, EX.MeatTopping, EX.CheeseTopping, EX.VegetarianPizza, EX.NonVegetarianPizza]for cls in classes: g.add((cls, RDF.type, OWL.Class))print(f"✓ {len(classes)} classes définies")
✓ 10 classes définies
Definition de la hiérarchie de classes OWL pour l’ontologie de test (Pizza Ontology simplifiee).
La hiérarchie est exprimée uniquement avec rdfs:subClassOf — la relation de spécialisation que tout raisonneur, même RDFS, propage des classes vers les instances. Deux arbres se dessinent : les bases (ThinAndCrispyBase, DeepPanBase sous PizzaBase) et les garnitures (MeatTopping sous PizzaTopping, CheeseTopping sous VegetarianTopping). La cellule ajoute aussi la propriété d’objet hasTopping avec son domaine et son intervalle, puis une restriction owl:allValuesFrom : c’est elle qui portera la charge sémantique des exemples guidés 4 et 6.
L’asymétrie de la hiérarchie des garnitures est volontaire et porte tout le poids sémantique du notebook : CheeseTopping descend de VegetarianTopping, mais MeatTopping n’en descend pas — le fromage est végétarien, la viande non. Cette branche déterminera chaque inférence ultérieure : le typage de mozzarella et de ham dans la cellule suivante, la restriction VegetarianPizza ⊑ ∀hasTopping.VegetarianTopping qui s’appuie sur elle, et les divergences DL/RL des exemples guidés 4 (incohérence de la tomate) et 6 (classification MeatPizza via someValuesFrom).
Ajout des individus (instances de classes) pour alimenter le raisonnement.
Les individus alimentent le raisonnement : sans instances, une ontologie ne produit que des inférences schéma contre schéma. Chaque pizza crée ici l’occasion d’une double inférence — son type direct et les types hérités par la chaîne de subClassOf — et les garnitures rattachées préparent la restriction allValuesFrom posée à la cellule précédente.
Deux individus concentreront l’attention des raisonneurs : margherita (VegetarianPizza, garnie de mozzarella et tomato) et pepperoni (NonVegetarianPizza, garnie de mozzarella et ham). Les types assertés de leurs garnitures — mozzarella a CheeseTopping, donc VegetarianTopping par héritage, tandis que ham a MeatTopping — constituent toute la matière première des classifications automatiques observées ensuite : un raisonneur OWL ne lira jamais rien d’autre que ces triplets, et chaque inférence de la suite découlera de leur propagation le long de la hiérarchie.
# Ajout d'instances pour tester l'inférence# Individusindividuals = [ (EX.myPizza, EX.Pizza), (EX.margherita, EX.VegetarianPizza), (EX.pepperoni, EX.NonVegetarianPizza), (EX.mozzarella, EX.CheeseTopping), (EX.tomato, EX.VegetarianTopping), (EX.ham, EX.MeatTopping), (EX.thinBase, EX.ThinAndCrispyBase), (EX.deepBase, EX.DeepPanBase),]for ind, cls in individuals: g.add((ind, RDF.type, cls))# Relationsg.add((EX.margherita, EX.hasTopping, EX.mozzarella))g.add((EX.margherita, EX.hasTopping, EX.tomato))g.add((EX.pepperoni, EX.hasTopping, EX.mozzarella))g.add((EX.pepperoni, EX.hasTopping, EX.ham))# Propriétés explicites pour tester la transitivitég.add((EX.CheeseTopping, RDFS.subClassOf, EX.PizzaTopping)) # Redondant mais utile# Propriétés RDFS pour l'inférence de typesg.add((EX.VegetarianPizza, OWL.equivalentClass, EX.VegetarianPizzaDef))g.add((EX.VegetarianPizzaDef, RDF.type, OWL.Class))print(f"✓ {len(individuals)} instances créées")print(f"✓ Total triples dans l'ontologie: {len(g)}")
✓ 8 instances créées
✓ Total triples dans l'ontologie: 40
Serialisation du graphe initial en Turtle comme reference avant l’application du raisonnement.
Sérialiser avant de raisonner fixe la référence : le fichier Turtle produit sert d’état « avant » pour vérifier, après coup, qu’aucun triplet inféré n’a contaminé le graphe de départ (chaque raisonneur travaillera sur une copie). L’aperçu affiché en sortie permet de contrôler d’un coup d’œil la forme des triplets — déclaration d’ontologie, typages, spécialisations — avant que les raisonneurs n’entrent en jeu.
Le compte de triplets de cet état « avant » (40) servira de dénominateur aux ratios d’expansion mesurés dans la section Comparaison : 6,3x pour owlrl (40 vers 251), ~2x pour reasonable (40 vers 82). Le fichier data/pizza_test.ttl joue aussi un rôle opérationnel : c’est l’entrée du seul raisonneur du notebook qui ne se pilote pas depuis Python — Growl, invoqué en ligne de commande sur ce fichier s’il est installé.
# Sauvegarde en Turtle pour referenceimport osos.makedirs("data", exist_ok=True)output_path ="data/pizza_test.owl"g.serialize(destination=output_path, format="turtle")print(f"* Ontologie sauvegardee: {output_path}")# Apercuprint("\n--- Apercu des triples (10 premiers) ---")for i, triple inenumerate(g):if i >=10:breakprint(triple)
owlrl est une implémentation pure Python des règles OWL 2 RL. Il s’intègre nativement avec RDFLib.
Caractéristiques
Langage: Python pur
OWL Profile: OWL 2 RL (règles Datalog)
Performance: Modérée (interprété Python)
Intégration: Native avec RDFLib (owlrl.OWLRL_Semantics)
Avantages: Facile à installer, pas de dépendance externe
Inconvénients: Plus lent que les implémentations compilées
Ce que cette implémentation peut et ne peut pas faire. owlrl fonctionne par matérialisation : il applique les règles OWL 2 RL (complétées des règles RDFS) en chaînage avant jusqu’à point fixe, et écrit chaque triplet déduit directement dans le graphe RDFLib. Le profil OWL 2 RL est précisément conçu pour que cette fermeture reste polynomiale — c’est le compromis qui le distingue d’OWL 2 DL, NExpTime-complet dans le pire cas. Deux conséquences observables dans ce notebook : owlrl ne détecte ni incohérence ontologique globale ni négation d’appartenance — il ne produit que des triplets positifs, et l’exemple 4 le montre matérialisant sans lever d’exception sur un graphe incohérent ; sur l’ontologie de test, il fait passer le graphe de 40 à 251 triplets (211 inférés) en une soixantaine de millisecondes, en dérivant notamment la réflexivité de subClassOf et le typage XSD complet (détaillés dans l’exemple 2).
Pourquoi commencer par lui. Python pur, sans dépendance externe, owlrl sert ici de référence sémantique : son implémentation exhaustive du corpus de règles du W3C fixe le volume maximal d’inférences dérivables du graphe. Les raisonneurs suivants, plus rapides, seront mesurés contre cette référence de complétude — c’est elle qui donnera leur sens aux écarts de triplets inférés observés dans la section Comparaison.
# Copie du graphe pour owlrlg_owlrl = Graph()for t in g: g_owlrl.add(t)# Raisonnement avec owlrlstart = time.time()owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_owlrl)time_owlrl = time.time() - starttriples_owlrl =len(g_owlrl)inferred_owlrl = triples_owlrl -len(g)print(f"✓ Temps owlrl: {time_owlrl:.4f} secondes")print(f"✓ Triples avant raisonnement: {len(g)}")print(f"✓ Triples après raisonnement: {triples_owlrl}")print(f"✓ Triples inférés: {inferred_owlrl}")
✓ Temps owlrl: 0.0587 secondes
✓ Triples avant raisonnement: 40
✓ Triples après raisonnement: 251
✓ Triples inférés: 211
Interpretation : Résultats du raisonnement owlrl
Sortie obtenue : Le raisonneur owlrl a traite l’ontologie en runtime machine-dep : ~60 ms, passant de 40 a 251 triples (211 triples inférés, ratio d’expansion 6.3x).
Métrique
Valeur
Signification
Temps d’exécution
runtime machine-dep : ~60 ms
Performance Python interprete (wall-clock, varie par machine)
Triples initiaux
40
Ontologie pizza manuelle
Affichage d’exemples de triplets inferences par owlrl pour illustrer les résultats du raisonnement OWL 2 RL.
Deux indices de lecture pour la sortie à venir : les triplets réflexifs (un terme relié à lui-même) et les triplets d’égalité sont la trace directe des règles OWL 2 RL de plus bas niveau. Ils paraissent triviaux — chacun conditionne pourtant la correction des inférences plus intéressantes qui s’appuient dessus.
# Exemples de triples inférésprint("--- Exemples de triples inférés par owlrl ---")inferred_triples =set(g_owlrl) -set(g)for i, t inenumerate(list(inferred_triples)[:15]):print(f" {t}")iflen(inferred_triples) >15:print(f" ... et {len(inferred_triples) -15} autres")
Sortie obtenue : owlrl a généré 211 triples inférés à partir des 40 triples originaux (ratio d’expansion 6.3x). Les 15 premiers exemples montrent la diversité des règles OWL 2 RL appliquées.
Catégorie de règles
Exemples de triples inférés
Règle OWL 2 RL correspondante
Types de données
xsd:int ⊑ rdfs:Datatype
Règles de typage XSD
Reflexivité
CheeseTopping ⊑ CheeseTopping
rdfs:subClassOf réflexif
Hiérarchie OWL
VegetarianPizza ⊑ owl:Thing
Classe ⊑ Thing
Identité
myPizza ≡ myPizza
owl:sameAs réflexif
Restrictions
margherita ⊑ VegetarianRestriction
Application de ∀hasTopping.VegetarianTopping
Points clés : 1. Complétude OWL 2 RL : owlrl implémente les ~300 règles de la spécification W3C OWL 2 RL 2. Inférences transitives : si A ⊑ B et B ⊑ C, alors A ⊑ C (transitivité de rdfs:subClassOf) 3. Typage automatique : tous les types XSD sont déclarés comme rdfs:Datatype 4. Application des restrictions : la restriction ∀hasTopping.VegetarianTopping est correctement appliquée à margherita
Note technique : owlrl suit strictement la spécification W3C OWL 2 RL (https://www.w3.org/TR/owl2-rl/). Les règles sont implémentées sous forme de règles Datalog qui peuvent être exécutées en temps polynomial. Les 211 triples inférés incluent non seulement les déductions sémantiques sur l’ontologie pizza, mais aussi toutes les règles de typage XSD, les axiomes OWL standards (sameAs réflexif, subClassOf transitif), et les inférences de propriétés. C’est pourquoi le nombre de triples est beaucoup plus élevé qu’avec reasonable.
Raisonneur 2: OWLReady2 + HermiT
OWLReady2 est un package Python qui permet d’utiliser le raisonneur HermiT (Java) depuis Python.
HermiT est un raisonneur OWL 2 DL complet, conforme à la sémantique directe d’OWL 2. Sa caractéristique distinctive est l’algorithme de hypertableau, qui réduit le nombre de branchements lors de la satisfiabilité. Description de référence : Glimm, Horrocks, Motik, Stoilos, Wang, HermiT: An OWL 2 Reasoner (Journal of Automated Reasoning, 2014).
Caractéristiques
Langage: Python avec bridge vers Java
OWL Profile: OWL 2 DL complet
Raisonneur: HermiT (très performant, tableaux)
Intégration: pip install owlready2
Avantages: OWL 2 DL complet, très performant
Inconvénients: Dépend Java, installation plus lourde
Ce que cette implémentation peut et ne peut pas faire. HermiT n’applique pas des règles : il décide de la satisfiabilité en construisant un modèle de l’ontologie — l’algorithme d’hypertableau, qui fusionne les individus pour limiter les branchements. C’est ce qui lui permet de couvrir OWL 2 DL complet — négation, cardinalités qualifiées, disjonctions — au prix d’une complexité théorique NExpTime-complète, que les ontologies réelles supportent toutefois bien mieux que le pire cas. Les services qu’il rend et que les raisonneurs RL de ce notebook ne rendent pas : détection d’incohérence (exemple 4 : tomate devient contradictoire dès que la disjonction Vegetal/Animal est assertée) et classification complète avec réponses négatives — l’exemple 6 montre que la cardinalité BigFamily = hasChild min 2 n’est dérivée que côté DL.
Comment lire sa mesure. Le temps affiché (~1,4 s) inclut le démarrage de la JVM et la conversion vers le monde OWLReady2 — les appels suivants sont nettement plus rapides. Les inférences, elles, se consultent via les attributs is_a des individus et les requêtes de classification d’OWLReady2 : elles ne sont pas écrites dans le graphe RDF exporté (43 triplets de structure), d’où le « N/A » des tableaux comparatifs dans la colonne des triplets inférés.
try:from owlready2 import*import owlready2# Limiter la memoire JVM du raisonneur pour compatibilite JVM 32-bit (defaut owlready2 = 2000 Mo) owlready2.reasoning.JAVA_MEMORY =1000 OWLREADY2_AVAILABLE =Trueprint("✓ OWLReady2 est disponible")exceptImportErroras e: OWLREADY2_AVAILABLE =Falseprint(f"⚠ OWLReady2 n'est pas disponible: {e}")print(" Installation: pip install owlready2")
✓ OWLReady2 est disponible
Creation des classes et proprietes OWLReady2 pour reproduire l’ontologie avec le raisonneur HermiT.
OWLReady2 expose un modèle objet différent du graphe RDFLib : les classes sont des classes Python (l’héritage natif code subClassOf), et le raisonnement s’appelle par synchronisation plutôt que par expansion du graphe. Reconstruire l’ontologie dans ce modèle est nécessaire — les deux mondes ne partagent pas leurs structures — et c’est aussi l’occasion de vérifier que la hiérarchie exprimée en triplets se traduit sans perte en déclarations Python.
if OWLREADY2_AVAILABLE:# Création de l'ontologie dans OWLReady2 onto = get_ontology("http://example.org/pizza#")with onto:# Classesclass Pizza(Thing):passclass PizzaBase(Thing):passclass ThinAndCrispyBase(PizzaBase):passclass DeepPanBase(PizzaBase):passclass PizzaTopping(Thing):passclass VegetarianTopping(PizzaTopping):passclass MeatTopping(PizzaTopping):passclass CheeseTopping(VegetarianTopping):pass# Définir la propriété d'abordclass has_topping(Pizza >> PizzaTopping):pass# Puis définir VegetarianPizza avec la restriction# La syntaxe correcte utilise is_a directement dans la classeclass VegetarianPizza(Pizza): is_a = [has_topping.some(VegetarianTopping)]class NonVegetarianPizza(Pizza):passprint("✓ Ontologie créée dans OWLReady2")
✓ Ontologie créée dans OWLReady2
Ajout des instances dans l’ontologie OWLReady2 et preparation du raisonnement HermiT.
Les mêmes instances que côté RDFLib sont recréées ici, avec leurs garnitures. Ce doublon volontaire est le prix de la comparaison : chaque raisonneur recevra une ontologie construite dans son idiome natif, sans conversion qui puisse introduire de biais.
if OWLREADY2_AVAILABLE:# Ajout d'instances mozzarella = CheeseTopping("Mozzarella") tomato = VegetarianTopping("Tomato") ham = MeatTopping("Ham") margherita = VegetarianPizza("Margherita") margherita.has_topping = [mozzarella, tomato] pepperoni = NonVegetarianPizza("Pepperoni") pepperoni.has_topping = [mozzarella, ham]print(f"✓ Instances créées")print(f" - Margherita a {len(margherita.has_topping)} toppings")print(f" - Pepperoni a {len(pepperoni.has_topping)} toppings")
✓ Instances créées
- Margherita a 2 toppings
- Pepperoni a 2 toppings
Exécution du raisonnement HermiT sur l’ontologie et collecte des résultats inferences.
sync_reasoner() lance le raisonneur HermiT embarqué par OWLReady2 : le pont sérialise l’ontologie vers la JVM, exécute le raisonneur, puis réimporte les classifications obtenues. Le premier appel embarque le démarrage de la JVM — les appels suivants sur la même session seraient plus rapides — et c’est pour cela que la comparaison finale de ce notebook mesure HermiT à part des raisonneurs RL.
if OWLREADY2_AVAILABLE:# Raisonnement avec HermiTprint("Démarrage du raisonnement HermiT...") start = time.time()try:# sync_reasoner() utilise HermiT par défaut# Nécessite Java d'installéwith onto:# debug=0 supprime l'echo par owlready2 de la commande Java# complete (classpath incluant le chemin d'install machine# C:\\Users\\...\\site-packages\\owlready2\\hermit ; ...).# secrets-hygiene regle 6 : fix cause-level, pas de scrub. sync_reasoner(debug=0) time_hermit = time.time() - start# Compter les inférences# OWLReady2 stocke les inférences directement dans les instancesprint(f"✓ Temps HermiT (via OWLReady2): {time_hermit:.4f} secondes")# Vérifier les types inférésprint("\n--- Types inférés pour Margherita ---")print(f" Classes directes: {margherita.is_a}")# Export en RDF pour comparer g_hermit = onto.world.as_rdflib_graph() triples_hermit =len(g_hermit)print(f"\n✓ Total triples (HermiT): {triples_hermit}")except (FileNotFoundError, OSError) as e:print(f"⚠ HermiT n'est pas disponible (Java manquant?): {e}")print(" HermiT nécessite Java Runtime Environment (JRE)") time_hermit =None triples_hermit =Noneelse: time_hermit =None triples_hermit =Noneprint("⚠ Test HermiT skip (OWLReady2 non disponible)")
Démarrage du raisonnement HermiT...
✓ Temps HermiT (via OWLReady2): 1.4094 secondes
--- Types inférés pour Margherita ---
Classes directes: [pizza.VegetarianPizza]
✓ Total triples (HermiT): 43
Interpretation : Résultats du raisonneur HermiT (OWL 2 DL)
Sortie obtenue : HermiT a termine le raisonnement en runtime machine-dep : ~1,4 s, produisant 43 triples au total.
Métrique
Valeur
Analyse
Temps d’exécution
runtime machine-dep : ~1,4 s
Plus lent qu’owlrl (inclus cold start JVM)
Triples totaux
43
Structure OWL de base (vs 251 avec owlrl)
HermiT est l’un des raisonneurs OWL 2 DL les plus performants. Il utilise un algorithme de tableaux optimisé.
Note: Le temps d’affichage peut inclure la première initialisation de la JVM. Les appels suivants sont beaucoup plus rapides.
Raisonneur 3: reasonable
reasonable est un raisonneur OWL 2 RL écrit en Rust avec des bindings Python.
Ce que cette implémentation peut et ne peut pas faire. reasonable exécute la même fermeture de règles OWL 2 RL qu’owlrl — même profil, même garantie polynomiale — mais en Rust : règles compilées et parcours de graphe optimisés réduisent le coût constant d’un facteur ~40 sur l’ontologie de test (~1,5 ms contre ~60 ms), et l’écart croît avec la taille du graphe — ~72x sur l’ontologie étendue de l’exemple 1 (0,041 s contre 2,97 s). Sa couverture est pragmatique : 42 triplets inférés contre 211 pour owlrl, l’écart portant sur les règles optionnelles (réflexivité de subClassOf, typage XSD exhaustif) et non sur les axiomes métier — la classification someValuesFrom de l’exemple 6 (pizza1 vers MeatPizza) est dérivée identiquement des deux côtés.
Pourquoi il vient juste après owlrl. Le couple owlrl/reasonable illustre un point que l’on retrouvera dans l’exemple 2 : être conforme à OWL 2 RL ne garantit pas des sorties identiques. La spécification laisse le choix des règles optionnelles, et chaque moteur tranche selon ses priorités — exhaustivité côté owlrl, vitesse côté reasonable. C’est cette marge de manœuvre que quantifient les benchmarks de la section Comparaison.
try:from reasonable import PyReasoner REASONABLE_AVAILABLE =Trueprint("✓ reasonable est disponible")exceptImportErroras e: REASONABLE_AVAILABLE =Falseprint(f"⚠ reasonable n'est pas disponible: {e}")print(" Installation: pip install reasonable")
✓ reasonable est disponible
Exécution du raisonnement avec la bibliotheque reasonable et collecte des résultats.
Même protocole que pour owlrl : copie du graphe de base, chronométrage de l’expansion, comptage des triplets avant et après. La symétrie des cellules est délibérée — seule l’implémentation change, pas la tâche — c’est elle qui autorise la comparaison du tableau final.
if REASONABLE_AVAILABLE:# Copie du graphe pour reasonable g_reasonable = rdflib.Graph()for t in g: g_reasonable.add(t)# Raisonnement avec reasonable (PyReasoner API) start = time.time()# PyReasoner retourne les triples inférés sans modifier le graphe reasoner = PyReasoner() reasoner.from_graph(g_reasonable) inferred_triples = reasoner.reason()for s, p, o in inferred_triples: g_reasonable.add((s, p, o)) time_reasonable = time.time() - start triples_reasonable =len(g_reasonable) inferred_reasonable = triples_reasonable -len(g)print(f"✓ Temps reasonable: {time_reasonable:.4f} secondes")print(f"✓ Triples avant raisonnement: {len(g)}")print(f"✓ Triples après raisonnement: {triples_reasonable}")print(f"✓ Triples inférés: {inferred_reasonable}")else: time_reasonable =None triples_reasonable =None inferred_reasonable =Noneprint("⚠ Test reasonable skip (non disponible)")
✓ Temps reasonable: 0.0015 secondes
✓ Triples avant raisonnement: 40
✓ Triples après raisonnement: 82
✓ Triples inférés: 42
Interpretation : Résultats du raisonneur reasonable
Sortie obtenue : Le raisonneur reasonable a infere 42 triples en runtime machine-dep : ~1,5 ms (40 -> 82 triples, ratio d’expansion ~2x).
Métrique
Valeur
Comparaison
Temps d’exécution
runtime machine-dep : ~1,5 ms
~40x plus rapide qu’owlrl (runtime machine-dep : ~60 ms)
Triples inférés
42
~5x moins qu’owlrl (211)
Ratio d’expansion
~2x
Sémantique substantielle
Langage
Rust compilé
Performance native
Points cles : 1. Vitesse et complétude combinées : reasonable (Rust compilé) reste le raisonneur le plus rapide tout en inférant 42 triples – soit l’essentiel des inférences utiles d’owlrl, pour ~40x moins de temps 2. Inférences proches d’owlrl : 42 triples inférés vs 211 pour owlrl – reasonable couvre la majorité des règles OWL 2 RL utiles en pratique, l’écart porte surtout sur les inférences de type/sous-type transitifs exhaustifs 3. Philosophie pragmatique : reasonable cible les règles OWL 2 RL les plus fréquemment utiles, optimisées pour la performance Rust, plutôt que la spécification W3C exhaustive (~300 règles) 4. Cas d’usage idéal : applications temps réel, microservices, architectures serverless où l’on veut à la fois rapidité ET une bonne couverture sémantique
Note technique : reasonable utilise l’algorithme de chase optimisé pour OWL 2 RL. Contrairement a owlrl qui implémente toutes les règles W3C (~300 règles), reasonable se concentre sur les règles les plus fréquemment utilisées. L’écart d’inférences (42 vs 211) porte principalement sur les fermetures transitives exhaustives. Pour la plupart des applications pratiques, reasonable suffit largement ; si vous avez besoin de complétude sémantique totale (ex: validation d’ontologie), préférez owlrl.
Raisonneur 4: Growl (optionnel)
Growl est un raisonneur OWL 2 RL écrit en C, vérifié formellement avec Z3.
Note: Growl nécessite une compilation depuis les sources. Ce notebook inclut le code pour l’utiliser s’il est installé.
Positionnement : le raisonnement vérifié. Les 76 règles (sur 78) de la spécification OWL 2 RL implémentées par Growl ont été prouvées avec le solveur SMT Z3 : chaque règle est accompagnée d’une preuve que sa sortie correspond à la sémantique W3C. La garantie est d’une autre nature que les tests empiriques d’owlrl ou de reasonable — pour un système critique (certification, médical), l’absence de bug de matérialisation se démontre, elle ne se constate pas à l’usage.
Statut dans ce notebook. Growl n’est pas installé dans l’environnement d’exécution : la cellule suivante le signale et les benchmarks l’excluent explicitement (time_growl = None). Les comparaisons chiffrées de la section suivante portent donc sur owlrl, reasonable et HermiT ; Growl reste dans le paysage pour sa valeur conceptuelle — la vérification formelle — et pour son coût d’intégration réel : pas d’API Python, une interface CLI invoquée par subprocess sur le fichier Turtle sérialisé.
import subprocessimport shutilimport os# Vérifier si Growl est disponibleGROWL_AVAILABLE = shutil.which("growl") isnotNoneif GROWL_AVAILABLE:print("✓ Growl est disponible")else:print("⚠ Growl n'est pas disponible")print(" Installation: https://github.com/S Toby Jacobs/growl")print(" Commandes typiques:")print(" git clone https://github.com/S-Toby-Jacobs/growl")print(" cd growl && make")print(" sudo make install")
⚠ Growl n'est pas disponible
Installation: https://github.com/S Toby Jacobs/growl
Commandes typiques:
git clone https://github.com/S-Toby-Jacobs/growl
cd growl && make
sudo make install
Verification de la disponibilite de Growl et exécution du raisonnement si disponible.
Growl se détecte par sa présence dans le PATH (shutil.which), pas par un import : c’est un exécutable C appelé en sous-processus, avec fichier d’entrée et de sortie sérialisés en Turtle. S’il est absent, la cellule imprime la procédure de compilation et le notebook continue — Growl est le seul des quatre raisonneurs qui exige une installation manuelle, et le tableau comparatif final saura ignorer proprement son absence.
if GROWL_AVAILABLE:# Preparer les fichiers pour Growl input_file ="data/pizza_test.ttl" output_file ="data/pizza_test_inferred.ttl"# Sauvegarder en Turtle g.serialize(destination=input_file, format="turtle")# Executer Growl start = time.time()try: result = subprocess.run( ["growl", "-i", input_file, "-o", output_file], capture_output=True, text=True, timeout=30 ) time_growl = time.time() - startif result.returncode ==0:# Charger les resultats g_growl = Graph() g_growl.parse(output_file, format="turtle") triples_growl =len(g_growl) inferred_growl = triples_growl -len(g)print(f"* Temps Growl: {time_growl:.4f} secondes")print(f"* Triples avant raisonnement: {len(g)}")print(f"* Triples apres raisonnement: {triples_growl}")print(f"* Triples inferees: {inferred_growl}")else:print(f"! Erreur Growl: {result.stderr}") time_growl =None triples_growl =Noneexcept subprocess.TimeoutExpired:print("! Timeout Growl") time_growl =None triples_growl =Noneelse: time_growl =None triples_growl =Noneprint("Growl non disponible : test ignore (time_growl=None)")
Growl non disponible : test ignore (time_growl=None)
Comparaison et Benchmarks
Synthétisons les résultats de tous les raisonneurs. Trois axes structurent la comparaison, et ils ne se lisent pas avec la même unité :
Le temps (wall-clock), mesuré à l’identique pour chaque raisonneur sur une copie fraîche du graphe initial. Deux biais sont assumés d’entrée : le temps de HermiT inclut le démarrage de la JVM, et Growl est exclu faute d’installation (time_growl = None).
Le volume inféré — le nombre de triplets dérivés, comparable uniquement entre raisonneurs du même profil RL (owlrl contre reasonable). Pour HermiT, la valeur est notée N/A : ses inférences ne sont pas matérialisées en RDF.
L’expressivité (RL contre DL) — elle ne se mesure pas en millisecondes mais en services : détection d’incohérence et réponses négatives existent seulement côté DL.
Ce que l’on attend. Sur un même profil RL, l’écart entre une implémentation interprétée (Python) et une compilée (Rust) se compte en ordre de grandeur plutôt qu’en pourcents — les mesures précédentes (~60 ms contre ~1,5 ms) l’annoncent déjà, et l’exemple guidé 1 le confirmera à plus grande échelle. Entre profils, la comparaison bascule : la question n’est plus « qui est plus rapide ? » mais « quel service est rendu ? ». Les deux figures et le tableau qui suivent séparent volontairement ces deux lectures.
Méthode. Chaque mesure part d’une copie fraîche du graphe sérialisé (aucun raisonneur ne voit les triplets inférés d’un autre), et les temps affichés sont des mesures machine-dépendantes à prendre en ordre de grandeur — c’est le facteur entre raisonneurs qui est stable d’une machine à l’autre, pas la valeur absolue. C’est pourquoi les interprétations de ce notebook qualifient chaque temps de runtime machine-dep.
=== Tableau comparatif ===
Raisonneur Langage OWL Profile Temps (s) Triples Inférés
owlrl Python RL 0.058716 251 211.0
HermiT Java DL 1.409404 43 NaN
reasonable Rust RL 0.001545 82 42.0
Interpretation : Tableau comparatif des raisonneurs
Sortie obtenue : DataFrame pandas (cellule précédente) comparant les deux raisonneurs OWL 2 RL — owlrl (Python) et reasonable (Rust) — sur temps, nombre de triples et triples inférés. HermiT (profil OWL 2 DL, mesuré séparément en cellule 30) est ajouté au tableau ci-dessous pour référence, mais n’appartient pas au DataFrame comparatif : profil et type de sortie différents (cf. note technique, et cellule suivante).
Raisonneur
Profil OWL
Temps
Triples finaux
Inférés
Ratio d’expansion
owlrl
RL
runtime machine-dep : ~60 ms
251
211
6.3x
HermiT
DL
runtime machine-dep : ~1,4 s
43
N/A
1.1x (structure)
reasonable
RL
runtime machine-dep : ~1,5 ms
82
42
~2x
Points cles : 1. Divergence d’approche : owlrl maximise les inférences (211 triples), reasonable reste pragmatique (42 triples) tout en étant le plus rapide 2. Profil OWL impacte la structure : HermiT (DL) ne produit pas le même type de sortie que les raisonneurs RL 3. Performance vs complétude : reasonable est ~40x plus rapide qu’owlrl tout en inférant ~5x moins de triples (mais reste substantiel : 42) 4. Cas d’usage : owlrl pour analyses sémantiques complètes, reasonable pour applications temps réel avec bonne couverture
Note technique : Les valeurs “Inférés” pour HermiT sont marquées N/A car OWLReady2 ne sépare pas les triples inférés des triples originaux de la même manière que RDFLib. La comparaison directe du nombre de triples entre HermiT et les raisonneurs RL n’est donc pas pertinente. Ce qui compte pour HermiT, c’est la capacité à raisonner sur OWL 2 DL complet (restrictions complexes, hiérarchies profondes).
Visualisation des temps d’exécution de chaque raisonneur sous forme de graphique.
Le graphique ne retient que les raisonneurs mesurés dans ce notebook. Chaque barre porte sa valeur numérique : à l’échelle des durées en jeu, la barre du plus rapide est presque invisible — c’est précisément le message visuel, et la cellule suivante le commente.
# Visualisation des temps d'exécutioniflen(results) >0: fig, ax = plt.subplots(figsize=(10, 5)) x =range(len(df)) bars = ax.bar(x, df["Temps (s)"], color=["#3498db", "#e74c3c", "#2ecc71", "#9b59b6"][:len(df)]) ax.set_xlabel("Raisonneur") ax.set_ylabel("Temps (secondes)") ax.set_title("Comparaison des temps d'exécution des raisonneurs OWL") ax.set_xticks(x) ax.set_xticklabels(df["Raisonneur"])# Ajouter les valeurs sur les barresfor bar in bars: height = bar.get_height() ax.text(bar.get_x() + bar.get_width()/2., height,f"{height:.4f}s", ha="center", va="bottom") plt.tight_layout() plt.show()else:print("Pas assez de données pour le graphique")
Interpretation : Visualisation des performances
Sortie obtenue : Graphique à barres montrant les temps d’exécution des deux raisonneurs OWL 2 RL compares (owlrl en Python et reasonable en Rust).
Raisonneur
Temps (s)
Classe de performance
Facteur vs plus rapide
reasonable
runtime machine-dep : ~1,5 ms
Ultra-rapide (Rust compile)
1x (reference)
owlrl
runtime machine-dep : ~60 ms
Modere (Python interprete)
~40x plus lent
Note : HermiT (cellule precedente, runtime machine-dep : ~1.4 s via OWLReady2) est mesure separement et n’apparait PAS dans ce graphique de comparaison : il implemente le profil OWL 2 DL (expressivite superieure) et passe par un cold start de la JVM, ce qui le rend non comparable directement aux deux raisonneurs OWL 2 RL ci-dessus.
Points cles : 1. Ordre de grandeur : reasonable (Rust) est ~40x plus rapide qu’owlrl (Python) sur le meme profil OWL 2 RL et la meme tache 2. Impact du langage : l’implementation compilee (Rust) surclasse largement l’implementation interpretee (Python) pour le meme algorithme de raisonnement 3. Profil d’usage : pour des raisonnements frequents en production, privilegier reasonable ; pour le prototypage pedagogique, owlrl suffit 4. Comparaison non-equivalente pour HermiT : HermiT (OWL 2 DL complet) est plus expressif mais plus lent (runtime machine-dep : ~1.4 s, dont une large part de cold start JVM) ; il n’est pas inclus dans la comparaison RL ci-dessus car il resout un probleme different (inferences plus riches)
Note technique : le temps d’HermiT (runtime machine-dep : ~1.4 s) inclut le cold start de la JVM. Les appels suivants seraient beaucoup plus rapides (runtime machine-dep : ~0.1-0.2 s). Pour des comparaisons equitables entre raisonneurs, il faudrait executer HermiT plusieurs fois et ne mesurer que les executions « a chaud ». Cependant, en pratique, le cold start est inevitable lors du premier raisonnement, ce qui explique pourquoi le benchmark ci-dessus se concentre sur les deux raisonneurs OWL 2 RL (mesures stables, sans cold start). Par ailleurs, les temps wall-clock absolus cites dans ce notebook (owlrl runtime machine-dep : ~0,06 s, reasonable runtime machine-dep : ~1,5 ms, HermiT runtime machine-dep : ~1.4 s) sont mesures sur une execution unique et varient avec la machine et la charge systeme ; seul l’ordre de grandeur (reasonable ≪ owlrl ≪ HermiT) est reproductible d’une machine a l’autre. Les valeurs numeriques exactes des sorties de code (cellules 16, 30, 37, 46) font foi ; un ecart de temps lors d’une re-execution n’est PAS une regression.
Comparaison du nombre de triplets inferences par les raisonneurs compatibles OWL 2 RL.
La seconde figure répond à une question distincte : non plus « qui va le plus vite ? » mais « qui déduit le plus ? ». Seuls les raisonneurs dont le compte de triplets inférés est comparable (profil RL) y figurent — les barres se lisent comme une mesure de complétude relative.
Avec une précaution, toutefois : 42 contre 211 triplets inférés ne veut pas dire « cinq fois moins de sémantique ». L’écart se concentre sur les règles optionnelles (réflexivité de subClassOf, typage XSD) et non sur les axiomes métier — l’exemple guidé 2 montrera que les classifications décisives sont identiques des deux côtés. La question applicative n’est donc pas « qui infère le plus ? » mais « le triplet dont mon application a besoin est-il produit ? » — et seule la vérification nominale (exemples 2 et 6) y répond.
# Comparaison des triples inférés (RL uniquement)rl_results = [r for r in results if r["OWL Profile"] =="RL"and r["Inférés"] isnotNone]iflen(rl_results) >=2: fig, ax = plt.subplots(figsize=(10, 5)) names = [r["Raisonneur"] for r in rl_results] inferred = [r["Inférés"] for r in rl_results] x =range(len(names)) bars = ax.bar(x, inferred, color="#3498db") ax.set_xlabel("Raisonneur") ax.set_ylabel("Nombre de triples inférés") ax.set_title("Triples inférés par raisonneur (OWL 2 RL)") ax.set_xticks(x) ax.set_xticklabels(names)for bar in bars: height = bar.get_height() ax.text(bar.get_x() + bar.get_width()/2., height,f"{int(height)}", ha="center", va="bottom") plt.tight_layout() plt.show()else:print("Pas assez de données RL pour la comparaison")
Interpretation : Comparaison des triples inférés (OWL 2 RL)
Sortie obtenue : Graphique à barres comparant le nombre de triples inférés par les raisonneurs OWL 2 RL compatibles (owlrl, reasonable).
Raisonneur
Triples inférés
Ratio d’expansion
Signification
owlrl
211
6.3x (40->251)
Implémente toutes les règles OWL 2 RL + RDFS
reasonable
42
~2x (40->82)
Couverture pragmatique des règles utiles
Points cles : 1. Écart modéré : owlrl infère ~5x plus de triples que reasonable (211 vs 42), l’écart porte sur les fermetures transitives exhaustives 2. Complétude vs pragmatisme : owlrl suit strictement la spécification W3C OWL 2 RL (~300 règles), reasonable se concentre sur les règles les plus courantes 3. Impact sur les applications : plus de triples inférés = meilleure complétude sémantique mais aussi plus de données à traiter 4. Choix du raisonneur : dépend de l’expressivité requise (owlrl pour complétude exhaustive, reasonable pour performance avec bonne couverture)
Note technique : La différence de nombre de triples inférés ne signifie pas que reasonable est “moins correct”. Les deux raisonneurs sont conformes à OWL 2 RL, mais owlrl inclut des règles optionnelles et des inférences de type/sous-type transitifs que l’implémentation reasonable ne dérive pas par défaut. Vérifiez toujours les triples spécifiques dont vous avez besoin pour votre application.
Analyse comparative
Performances. Sur le même profil OWL 2 RL, reasonable (~1,5 ms) devance owlrl (~60 ms) d’un facteur ~40 sur l’ontologie de test — et l’écart croît avec la taille du graphe : l’exemple guidé 1 mesure 0,041 s contre 2,97 s (939 contre 8005 triplets inférés), soit ~72x sur l’ontologie étendue. HermiT (~1,4 s, JVM comprise) n’entre pas dans cette course : son temps achète un service que les deux autres ne rendent pas — le raisonnement OWL 2 DL complet.
Couverture fonctionnelle. owlrl : OWL 2 RL exhaustif (règles optionnelles comprises, 211 triplets inférés), intégration RDFLib native. reasonable : OWL 2 RL pragmatique (42 triplets, classifications métier identiques sur tous les cas testés — exemples 2 et 6), performance Rust. HermiT : OWL 2 DL complet, seul à détecter les incohérences (exemple 4) et à dériver les cardinalités qualifiées (exemple 6). Growl : OWL 2 RL vérifié formellement (76/78 règles prouvées avec Z3), non mesuré ici faute d’installation.
Facilité d’intégration (échelle qualitative, de très haute à faible) : owlrl — très haute (pip install, natif avec RDFLib) ; reasonable — haute (pip install, bindings PyO3) ; OWLReady2 + HermiT — moyenne (nécessite une JVM) ; Growl — faible (compilation C manuelle, interface CLI uniquement).
Le compromis en une phrase. Aucun raisonneur ne domine sur les trois axes : le choix est une fonction du besoin — complétude de matérialisation (owlrl), vitesse à couverture utile (reasonable), services DL et garantie logique (HermiT), preuve formelle des règles (Growl).
Exemples guidés
Les exemples suivants illustrent des cas d’usage avancés des raisonneurs OWL. Solutions proposées par @Sosolalt (EPITA-IS, promo 2028).
Chaque exemple combine un scénario d’usage réaliste, une exécution complète dont les sorties sont committées, et — pour quatre d’entre eux — une lecture ancrée sur la sortie. Les énoncés d’exercices correspondants, en fin de notebook, reprennent les mêmes scénarios sur des squelettes à compléter.
Exemple guidé 1 : Benchmark sur une ontologie plus grande
Solution proposee par @Sosolalt (EPITA-IS, promo 2028).
L’ontologie de test de ce notebook compte une quarantaine de triplets — assez pour observer les mécanismes, trop peu pour distinguer des stratégies d’implémentation. Cet exemple rejoue donc le benchmark sur la Pizza Ontology complète de Stanford, servie depuis une copie locale vendored pour rester reproductible hors ligne (le téléchargement distant n’est qu’un repli). Le contraste avec les mesures sur petit graphe porte sur les ratios, pas sur les ordres de grandeur.
# Exemple guide 1 : Benchmark sur la Pizza Ontology complete# --------------------------------------------------------import timeimport copyimport osPIZZA_URL ="https://protege.stanford.edu/ontologies/pizza/pizza.owl"PIZZA_LOCAL ="data/pizza.owl"# copie vendoree (epinglee) — reproductible hors-ligne# Charger l'ontologie pizza : copie locale d'abord (reproductible, pas de fetch reseau),# fallback sur le fetch distant si la copie locale manque (robustesse #5033).g_pizza = rdflib.Graph()if os.path.exists(PIZZA_LOCAL): g_pizza.parse(PIZZA_LOCAL, format="xml")print(f"Pizza Ontology chargee (copie locale vendoree) : {len(g_pizza)} triples")else:try: g_pizza.parse(PIZZA_URL, format="xml")print(f"Pizza Ontology chargee (fetch distant, copie locale manquante) : {len(g_pizza)} triples")exceptExceptionas exc:raiseRuntimeError(f"Impossible de charger pizza.owl : copie locale '{PIZZA_LOCAL}' absente "f"et fetch distant echoue ({exc}). Le benchmark Scaling depend de cette ontologie." ) from exc# Serialiser une fois en N-Triples pour repartir d'une copie propre a chaque raisonnementbase_nt = g_pizza.serialize(format="nt")def fresh_pizza(): gg = rdflib.Graph() gg.parse(data=base_nt, format="nt")return ggbench = []# owlrlg_owlrl_pizza = fresh_pizza()start = time.time()owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_owlrl_pizza)t_owlrl_pizza = time.time() - startbench.append(("owlrl", t_owlrl_pizza, len(g_owlrl_pizza) -len(g_pizza)))# reasonable (si disponible)if REASONABLE_AVAILABLE: g_reasonable_pizza = fresh_pizza() start = time.time() reasoner = PyReasoner() reasoner.from_graph(g_reasonable_pizza)for s, p, o in reasoner.reason(): g_reasonable_pizza.add((s, p, o)) t_reasonable_pizza = time.time() - start bench.append(("reasonable", t_reasonable_pizza, len(g_reasonable_pizza) -len(g_pizza)))# Tableau comparatifprint()print(f"{'Raisonneur':<20}{'Temps (s)':<14}{'Triples inferes':<18}")print("-"*52)for name, t, inferred in bench:print(f"{name:<20}{t:<14.4f}{inferred:<18}")iflen(bench) >=2and bench[1][1] >0:print()print(f"reasonable est ~{bench[0][1] / bench[1][1]:.0f}x plus rapide qu'owlrl sur cette ontologie.")
Pizza Ontology chargee (copie locale vendoree) : 1944 triples
Raisonneur Temps (s) Triples inferes
----------------------------------------------------
owlrl 2.9682 8005
reasonable 0.0410 939
reasonable est ~72x plus rapide qu'owlrl sur cette ontologie.
Lecture de l’exemple 1 : le comportement à l’échelle
La sortie committée mesure, sur la Pizza Ontology complète (1944 triplets chargés depuis la copie locale vendored) :
Raisonneur
Temps mesuré
Triplets inférés
owlrl
runtime machine-dep : ~3 s
8005
reasonable
runtime machine-dep : ~0,04 s
939
Deux ordres de grandeur se confirment à l’échelle : l’écart de vitesse se creuse (~72x selon la sortie, contre ~40x sur l’ontologie de test d’une quarantaine de triplets), et l’écart de complétude se creuse aussi (8005 contre 939 triplets inférés, soit ~8,5x). Leçon méthodologique : un benchmark sur un petit graphe donne les tendances correctes mais sous-estime les deux écarts ; les ratios divergent avec la taille du graphe, et seul l’ordre de grandeur (reasonable bien plus rapide, owlrl bien plus complet) est stable d’une ontologie à l’autre.
Exemple guidé 2 : Verification de l’equivalence des résultats RL
Solution proposee par @Sosolalt (EPITA-IS, promo 2028).
Les deux raisonneurs RL revendiquent le même profil : leurs fermetures sont-elles identiques pour autant ? La cellule compare les ensembles de triplets inférés triplet par triplet, et détaille ce que chacun produit en propre.
# Exercice 2 : Verification de l'equivalence owlrl vs reasonable# Etape 1 : graphe de base + copies pour chaque raisonneurtriples_base =set(g)g_owlrl_copy = rdflib.Graph()for t in g: g_owlrl_copy.add(t)# Etape 2 : appliquer owlrlowlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_owlrl_copy)inferred_owlrl =set(g_owlrl_copy) - triples_base# Etape 2bis : appliquer reasonableif REASONABLE_AVAILABLE: g_reasonable_copy = rdflib.Graph()for t in g: g_reasonable_copy.add(t) reasoner = PyReasoner() reasoner.from_graph(g_reasonable_copy)for s, p, o in reasoner.reason(): g_reasonable_copy.add((s, p, o)) inferred_reasonable =set(g_reasonable_copy) - triples_baseelse: inferred_reasonable =set()print("reasonable non disponible : comparaison partielle.")# Etape 3 : difference symetriquediff = inferred_owlrl.symmetric_difference(inferred_reasonable)only_owlrl = inferred_owlrl - inferred_reasonableonly_reasonable = inferred_reasonable - inferred_owlrlprint("=== Comparaison owlrl vs reasonable (OWL 2 RL) ===")print(f" Triples inferes par owlrl : {len(inferred_owlrl)}")print(f" Triples inferes par reasonable : {len(inferred_reasonable)}")print(f" Triples communs : {len(inferred_owlrl & inferred_reasonable)}")print(f" Difference symetrique : {len(diff)}")# Etape 4 : afficher les ecartsprint(f"\n--- Uniquement owlrl ({len(only_owlrl)}), 5 exemples ---")for t inlist(only_owlrl)[:5]:print(f" {t}")print(f"\n--- Uniquement reasonable ({len(only_reasonable)}), 5 exemples ---")for t inlist(only_reasonable)[:5]:print(f" {t}")print("\nAnalyse : owlrl implemente l'integralite des regles W3C OWL 2 RL ""(reflexivite subClassOf, typage XSD, axiomes OWL standards),")print("tandis que reasonable se concentre sur les regles essentielles -> ""owlrl materialise beaucoup plus de triples.")
=== Comparaison owlrl vs reasonable (OWL 2 RL) ===
Triples inferes par owlrl : 211
Triples inferes par reasonable : 42
Triples communs : 26
Difference symetrique : 201
--- Uniquement owlrl (185), 5 exemples ---
(rdflib.term.URIRef('http://www.w3.org/2002/07/owl#Class'), rdflib.term.URIRef('http://www.w3.org/2002/07/owl#sameAs'), rdflib.term.URIRef('http://www.w3.org/2002/07/owl#Class'))
(rdflib.term.URIRef('http://example.org/pizza#PizzaBase'), rdflib.term.URIRef('http://www.w3.org/2000/01/rdf-schema#subClassOf'), rdflib.term.URIRef('http://example.org/pizza#PizzaBase'))
(rdflib.term.URIRef('http://www.w3.org/2001/XMLSchema#unsignedByte'), rdflib.term.URIRef('http://www.w3.org/2002/07/owl#sameAs'), rdflib.term.URIRef('http://www.w3.org/2001/XMLSchema#unsignedByte'))
(rdflib.term.URIRef('http://www.w3.org/2002/07/owl#Nothing'), rdflib.term.URIRef('http://www.w3.org/2000/01/rdf-schema#subClassOf'), rdflib.term.URIRef('http://www.w3.org/2002/07/owl#Thing'))
(rdflib.term.URIRef('http://www.w3.org/2000/01/rdf-schema#Literal'), rdflib.term.URIRef('http://www.w3.org/2002/07/owl#sameAs'), rdflib.term.URIRef('http://www.w3.org/2000/01/rdf-schema#Literal'))
--- Uniquement reasonable (16), 5 exemples ---
(rdflib.term.URIRef('http://example.org/pizza#PizzaOntology'), rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), rdflib.term.URIRef('http://www.w3.org/2002/07/owl#Thing'))
(rdflib.term.URIRef('http://example.org/pizza#MeatTopping'), rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), rdflib.term.URIRef('http://www.w3.org/2002/07/owl#Thing'))
(rdflib.term.URIRef('http://example.org/pizza#VegetarianPizzaDef'), rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), rdflib.term.URIRef('http://www.w3.org/2002/07/owl#Thing'))
(rdflib.term.URIRef('http://example.org/pizza#hasTopping'), rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), rdflib.term.URIRef('http://www.w3.org/2002/07/owl#Thing'))
(rdflib.term.URIRef('http://example.org/pizza#NonVegetarianPizza'), rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'), rdflib.term.URIRef('http://www.w3.org/2002/07/owl#Thing'))
Analyse : owlrl implemente l'integralite des regles W3C OWL 2 RL (reflexivite subClassOf, typage XSD, axiomes OWL standards),
tandis que reasonable se concentre sur les regles essentielles -> owlrl materialise beaucoup plus de triples.
Lecture de l’exemple 2 : une divergence qui n’en est pas vraiment une
La sortie ci-dessus répond crûment à la question « owlrl et reasonable produisent-ils les mêmes triples ? » : non. Mais le détail des triples révèle que ce « non » est trompeur.
Les chiffres bruts sont alarmants : owlrl infère 211 triples, reasonable 42, et leur intersection ne contient que 26 triples communs — d’où une différence symétrique de 201. Vu de loin, les deux raisonner OWL 2 RL semblent diverger massivement.
Mais la divergence est bidirectionnelle et, des deux côtés, constituée de bruit de bas niveau :
Aucune des deux listes ne contient du raisononnement au sens utile (classification d’instances, chaînes de propriétés, restrictions) — seulement des assertions structurelles que tout graphe OWL satisfait trivialement. owlrl materialise l’intégralité des règles W3C (réflexivité sameAs, typage XSD exhaustif) ; reasonable adopte une convention différente (expliciter l’appartenance à owl:Thing). Les deux approches sont cohérentes, elles diffèrent juste sur quelles tautologies expliciter.
Leçon : comparer des raisonner OWL 2 RL au nombre de triples inférés est trompeur. Ce qui compte n’est pas le volume, mais la couverture des règles substantielles (hiérarchie de classes, restrictions, consistance). Sur ce critère, owlrl et reasonable convergent : leurs 26 triples communs portent l’essentiel du raisonnement utile. La différence de 201 est un artefact de configuration, pas un désaccord sémantique.
Nuance vs la comparaison générique (§Comparaison) : le tableau cellule 53 expliquait l’écart par les « fermetures transitives exhaustives » — la lecture concrète ici précise qu’il s’agit en fait de réflexivité et de typage, et révèle un aspect que cette explication générique taisait : reasonable produit aussi des triples qu’owlrl ne produit pas (les 16 type owl:Thing). L’exercice 2 (indice cellule 75) n’adressait lui aussi que les extras d’owlrl.
Exemple guidé 3 : Profilage avance avec cProfile
Solution proposee par @Sosolalt (EPITA-IS, promo 2028).
Profiler complète le chronométrage : le temps total dit combien, le profil dit où. La cellule instrumente l’expansion owlrl avec cProfile et trie les fonctions par temps cumulé — les chemins de fichiers sont normalisés par strip_dirs() pour que la sortie reste portable.
# Exercice 3 : Profilage avance avec cProfileimport cProfileimport pstatsimport io# Etape 1 : copie du graphe de baseg_profile = rdflib.Graph()for t in g: g_profile.add(t)# Etape 2 : profiler le raisonnement owlrlpr = cProfile.Profile()pr.enable()owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_profile)pr.disable()# Etape 3 : afficher les 10 fonctions les plus couteuses (tri cumulatif)s = io.StringIO()# strip_dirs() (API native pstats) deroute les prefixes de chemin de chaque# fonction profilee. Sous papermill, cProfile enregistre le chemin temporaire# du kernel (~\AppData\Local\Temp\ipykernel_<pid>\NNNNN.py) comme source ;# on le neutralise a la cause plutot qu'en post-traitement de la sortie# (secrets-hygiene regle 6 : fixer la cause + re-executer, jamais scrubber).ps = pstats.Stats(pr, stream=s).strip_dirs().sort_stats("cumulative")ps.print_stats(10)print("=== Top 10 des fonctions les plus couteuses (owlrl, tri cumulatif) ===")print(s.getvalue())
=== Top 10 des fonctions les plus couteuses (owlrl, tri cumulatif) ===
304119 function calls (304118 primitive calls) in 0.153 seconds
Ordered by: cumulative time
List reduced from 93 to 10 due to restriction <10>
ncalls tottime percall cumtime percall filename:lineno(function)
3/2 0.000 0.000 0.153 0.076 interactiveshell.py:3712(run_code)
2 0.000 0.000 0.153 0.076 {built-in method builtins.exec}
1 0.000 0.000 0.153 0.153 1276945933.py:1(<module>)
1 0.000 0.000 0.153 0.153 __init__.py:384(expand)
820 0.002 0.000 0.146 0.000 OWLRL.py:320(rules)
1 0.000 0.000 0.087 0.087 Closure.py:276(closure)
820 0.007 0.000 0.076 0.000 OWLRL.py:372(_equality)
17397 0.010 0.000 0.062 0.000 graph.py:672(triples)
5813 0.004 0.000 0.054 0.000 Closure.py:255(store_triple)
6975 0.005 0.000 0.049 0.000 graph.py:790(__contains__)
Lecture de l’exemple 3 : où passe le temps d’owlrl
La sortie du profileur est explicite : 304 119 appels de fonctions pour un seul raisonnement, et le haut du classement par temps cumulé est occupé par OWLRL.py (la passe de règles, invoquée 820 fois) puis Closure.py (la boucle de fermeture). Autrement dit, le coût n’est ni dans l’analyse syntaxique du graphe ni dans les entrées-sorties : il est dans l’application répétée du corpus de règles OWL 2 RL jusqu’au point fixe. C’est la signature d’un moteur de règles interprété — chaque règle est réévaluée en Python pur à chaque tour — contre l’implémentation Rust de reasonable qui compile ce même cycle de règles. Le chiffre déterminant à retenir : 820 invocations de la passe de règles pour une quarantaine de triplets initiaux, soit plus de vingt passes par triplet de départ ; ce compte est déterministe pour une ontologie donnée, seule la durée varie avec la machine.
Exemple guidé 4 : Detection d’incoherence avec un raisonneur DL (HermiT)
Solution proposee par @Sosolalt (EPITA-IS, promo 2028).
Le scénario déclare volontairement une ontologie incohérente — une garniture typée à la fois végétale et animale via des classes disjointes — et demande aux deux familles de raisonneurs ce qu’elles en tirent. La sortie oppose un verdict logique et une matérialisation silencieuse ; la lecture qui suit la cellule en tire la règle de choix.
# Exercice 4 : Detection d'incoherence avec HermiT (DL) vs tolerance owlrl (RL)# --- Partie DL : HermiT via OWLReady2 ---if OWLREADY2_AVAILABLE:import shutilfrom owlready2 import (get_ontology, Thing, AllDisjoint, sync_reasoner_hermit, OwlReadyInconsistentOntologyError) java_path = shutil.which("java")if java_path: owlready2.JAVA_EXE = java_path# World isole : le raisonnement ne porte que sur cette ontologie world = owlready2.World() onto = world.get_ontology("http://test.org/disjoint.owl")with onto:class Vegetal(Thing):passclass Animal(Thing):pass AllDisjoint([Vegetal, Animal])# Instance contradictoire : a la fois Vegetal ET Animal tomate = Vegetal("tomate") tomate.is_a.append(Animal)try:# debug=0 : empeche owlready2 d'echoer la commande Java (classpath# = chemin d'install machine C:\\Users\\...\\owlready2\\hermit). sync_reasoner_hermit(world, debug=0)print("HermiT (DL) : aucune incoherence detectee (inattendu).")except OwlReadyInconsistentOntologyError:print("HermiT (DL) : INCOHERENCE detectee -> 'tomate' ne peut etre ""a la fois Vegetal et Animal (classes disjointes). Resultat attendu.")exceptExceptionas e:print(f"HermiT (DL) indisponible dans cet environnement ({type(e).__name__}).")print(" -> Le raisonneur Java ne s'execute pas ici ; partie DL non evaluee.")else:print("OWLReady2 non disponible : partie DL ignoree.")# --- Partie RL : owlrl sur le meme KG transcrit en rdflib ---from rdflib import Graph, Namespace, RDF, OWLEXD = Namespace("http://test.org/disjoint#")g_disj = Graph()g_disj.add((EXD.Vegetal, RDF.type, OWL.Class))g_disj.add((EXD.Animal, RDF.type, OWL.Class))g_disj.add((EXD.Vegetal, OWL.disjointWith, EXD.Animal))g_disj.add((EXD.tomate, RDF.type, EXD.Vegetal))g_disj.add((EXD.tomate, RDF.type, EXD.Animal))n_before =len(g_disj)owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_disj)print(f"\nowlrl (RL) : materialisation effectuee ({n_before} -> {len(g_disj)} triples), ""AUCUNE exception levee.")print("Le profil RL materialise les triples mais ne rejette pas l'incoherence ""comme le ferait un raisonneur DL complet.")print("\nConclusion :")print(" - DL (HermiT) : valide la coherence logique (detecte les contradictions).")print(" - RL (owlrl) : materialise rapidement la cloture, mais tolere l'incoherence.")print(" => DL pour la validation logique, RL pour la materialisation rapide.")
HermiT (DL) : INCOHERENCE detectee -> 'tomate' ne peut etre a la fois Vegetal et Animal (classes disjointes). Resultat attendu.
owlrl (RL) : materialisation effectuee (5 -> 126 triples), AUCUNE exception levee.
Le profil RL materialise les triples mais ne rejette pas l'incoherence comme le ferait un raisonneur DL complet.
Conclusion :
- DL (HermiT) : valide la coherence logique (detecte les contradictions).
- RL (owlrl) : materialise rapidement la cloture, mais tolere l'incoherence.
=> DL pour la validation logique, RL pour la materialisation rapide.
Lecture de l’exemple 4 : DL détecte, RL matérialise
La sortie oppose les deux philosophies sur le même graphe incohérent (tomate déclarée à la fois Vegetal et Animal, classes disjointes) : HermiT (DL) signale l’INCOHÉRENCE, owlrl (RL) produit sa matérialisation sans lever d’exception. La détection d’HermiT ne paraît arbitraire que si l’on oublie l’hypothèse du monde ouvert (cf. section liminaire) : c’est l’assertion explicite de disjointness qui rend la contradiction dérivable, et seul un raisonneur complet au sens OWL 2 DL a l’obligation de la signaler. Le profil RL, conçu pour la matérialisation rapide, applique ses règles sans contrôle de cohérence global — un graphe incohérent produit simplement des triplets contradictoires. Conclusion pratique, cohérente avec le tableau comparatif : raisonneur DL pour valider la cohérence logique d’une ontologie avant publication, profil RL pour dériver vite une fermeture exploitable.
Exemple guidé 5 : Inferences sur axiomes de proprietes OWL
Solution proposee par @Sosolalt (EPITA-IS, promo 2028).
Au-delà des classes, OWL contraint aussi les propriétés : transitivité, symétrie, inversion. La cellule asserte un petit graphe familial puis regarde quels triplets le raisonnement matérialise — y compris une chaîne d’ancêtres dont la fermeture transitive se compte exactement.
# Exercice 5 : Inferences sur axiomes de proprietes OWLfrom rdflib import Graph, Namespace, RDF, OWLEXR = Namespace("http://test.org/props#")g_props = Graph()g_props.bind("ex", EXR)g_props.bind("owl", OWL)# hasAncestor : TransitiveProperty, chaine A -> B -> C (attendu : A -> C)g_props.add((EXR.hasAncestor, RDF.type, OWL.TransitiveProperty))g_props.add((EXR.A, EXR.hasAncestor, EXR.B))g_props.add((EXR.B, EXR.hasAncestor, EXR.C))# knows : SymmetricProperty, (A knows B) (attendu : B knows A)g_props.add((EXR.knows, RDF.type, OWL.SymmetricProperty))g_props.add((EXR.A, EXR.knows, EXR.B))# hasParent inverseOf hasChild, (A hasParent B) (attendu : B hasChild A)g_props.add((EXR.hasParent, OWL.inverseOf, EXR.hasChild))g_props.add((EXR.A, EXR.hasParent, EXR.B))# Appliquer owlrln_avant =len(g_props)owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_props)n_apres =len(g_props)print(f"Triples avant: {n_avant}, apres: {n_apres}, materialises: {n_apres - n_avant}")# Verifier les 3 inferences attendues via SPARQL ASKr1 = g_props.query(f"ASK {{ <{EXR.A}> <{EXR.hasAncestor}> <{EXR.C}> }}")r2 = g_props.query(f"ASK {{ <{EXR.B}> <{EXR.knows}> <{EXR.A}> }}")r3 = g_props.query(f"ASK {{ <{EXR.B}> <{EXR.hasChild}> <{EXR.A}> }}")print(f"\n Transitive (A hasAncestor C) : {bool(r1)}")print(f" Symetrique (B knows A) : {bool(r2)}")print(f" Inverse (B hasChild A) : {bool(r3)}")# Bonus : chaine transitive de 10 elements -> combien de hops materialises ?g_chain = Graph()g_chain.add((EXR.hasAncestor, RDF.type, OWL.TransitiveProperty))nodes = [EXR[f"n{i}"] for i inrange(10)]for i inrange(9): g_chain.add((nodes[i], EXR.hasAncestor, nodes[i +1]))base_chain =len(g_chain) -1# hors declaration de proprieteowlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_chain)pairs =sum(1for _ in g_chain.triples((None, EXR.hasAncestor, None)))# n*(n-1)/2 paires pour une chaine de 10 elements = 45print(f"\n Bonus chaine de 10 elements : {base_chain} aretes initiales -> "f"{pairs} paires hasAncestor materialisees (cloture transitive complete = 45).")
Triples avant: 7, apres: 126, materialises: 119
Transitive (A hasAncestor C) : True
Symetrique (B knows A) : True
Inverse (B hasChild A) : True
Bonus chaine de 10 elements : 9 aretes initiales -> 45 paires hasAncestor materialisees (cloture transitive complete = 45).
Lecture de l’exemple 5 : la fermeture des axiomes de propriétés
Sept triplets initiaux deviennent 126 après raisonnement — 119 triplets matérialisés — et les trois propriétés attendues tombent à True : transitivité (A hasAncestor C dérivé des deux arcs A vers B puis B vers C), symétrie (B knows A dérivé de A knows B) et inversion (B hasChild A dérivé de A hasParent B). Le bonus quantifie le coût de la fermeture transitive : une chaîne de dix éléments (neuf arêtes) se dilate en 45 paires hasAncestor — exactement 9+8+…+1, soit 10·9/2 — le nombre de paires d’une chaîne de dix éléments. Ce compte est déterministe et illustre pourquoi la matérialisation complète peut exploser sur de grands graphes : la fermeture transitive croît quadratiquement avec la longueur de la plus longue chaîne, ce qui est une des raisons de préférer, dans certaines applications, l’interrogation du raisonneur à la demande plutôt que la matérialisation de toute la fermeture.
Exemple guidé 6 : Comparaison expressivite OWL 2 RL vs OWL 2 DL (someValuesFrom)
Solution proposee par @Sosolalt (EPITA-IS, promo 2028).
L’opinion répandue veut que la classification par restriction someValuesFrom soit hors de portée du profil RL. La cellule la teste sur deux constructions — la classification d’une pizza, puis une cardinalité minimale — et la sortie nuance sérieusement la légende.
# Exercice 6 : OWL 2 RL vs OWL 2 DL sur someValuesFrom (classification automatique)# --- Partie DL : HermiT via OWLReady2 ---meatpizza_dl =Noneif OWLREADY2_AVAILABLE:try:import shutilfrom owlready2 import (get_ontology, Thing, ObjectProperty, sync_reasoner_hermit) java_path = shutil.which("java")if java_path: owlready2.JAVA_EXE = java_path# World isole : evite toute contamination par les ontologies des autres cellules world = owlready2.World() onto = world.get_ontology("http://test.org/pizza2.owl")with onto:class Pizza(Thing):passclass Topping(Thing):passclass MeatTopping(Topping):passclass hasTopping(ObjectProperty): domain = [Pizza]range= [Topping]class MeatPizza(Pizza): equivalent_to = [Pizza & hasTopping.some(MeatTopping)] topping1 = MeatTopping("topping1") pizza1 = Pizza("pizza1") pizza1.hasTopping.append(topping1)# debug=0 : empeche owlready2 d'echoer la commande Java (classpath# = chemin d'install machine C:\\Users\\...\\owlready2\\hermit). sync_reasoner_hermit(world, debug=0) meatpizza_dl = MeatPizza in pizza1.is_aprint(f"HermiT (DL) : pizza1 classifiee comme MeatPizza ? {meatpizza_dl}")exceptExceptionas e:print(f"HermiT (DL) indisponible dans cet environnement ({type(e).__name__}).")else:print("OWLReady2 non disponible : partie DL ignoree.")# --- Partie RL : meme KG en pur rdflib + owlrl ---from rdflib import Graph, Namespace, RDF, RDFS, OWL, BNodeEXM = Namespace("http://test.org/pizza#")g_rl = Graph()g_rl.add((EXM.MeatTopping, RDFS.subClassOf, EXM.Topping))bn = BNode()g_rl.add((bn, RDF.type, OWL.Restriction))g_rl.add((bn, OWL.onProperty, EXM.hasTopping))g_rl.add((bn, OWL.someValuesFrom, EXM.MeatTopping))g_rl.add((EXM.MeatPizza, OWL.equivalentClass, bn))g_rl.add((EXM.pizza1, RDF.type, EXM.Pizza))g_rl.add((EXM.topping1, RDF.type, EXM.MeatTopping))g_rl.add((EXM.pizza1, EXM.hasTopping, EXM.topping1))owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_rl)meatpizza_rl = (EXM.pizza1, RDF.type, EXM.MeatPizza) in g_rlprint(f"owlrl (RL) : pizza1 classifiee comme MeatPizza ? {meatpizza_rl}")# --- Tableau comparatif ---print("\n| Profil | Classification automatique pizza1 -> MeatPizza ? |")print("|--------------|---------------------------------------------------|")print(f"| RL (owlrl) | {meatpizza_rl}")print(f"| DL (HermiT) | {meatpizza_dl if meatpizza_dl isnotNoneelse'non execute (HermiT indispo)'}")print("\nNote : contrairement a une idee repandue, owlrl implemente la regle ""cls-svf1 d'OWL 2 RL,")print("ce qui lui permet ICI de deriver la classification via someValuesFrom ""(equivalentClass + restriction).")print("La difference RL/DL se manifeste sur des constructions plus riches ""(cardinalites, unions, classification complete)")print("que le profil RL ne couvre pas et que seul un raisonneur DL comme ""HermiT traite integralement.")# === Contraste reel RL vs DL : classification par cardinalite (hasChild min 2) ===# Le profil OWL 2 RL interdit les cardinalites minimales en position de# super-classe : la classification ci-dessous n'est PAS derivee par owlrl,# alors qu'HermiT (DL) la derive.print("\n"+"="*60)print("Contraste reel : BigFamily = (hasChild min 2)")print("="*60)bigfamily_dl =Noneif OWLREADY2_AVAILABLE:try:from owlready2 import Thing as _Thing, ObjectProperty as _OP, AllDifferent world2 = owlready2.World() onto2c = world2.get_ontology("http://test.org/family.owl")with onto2c:class Person(_Thing):passclass hasChild(_OP):passclass BigFamily(Person): equivalent_to = [Person & hasChild.min(2, Person)] parent = Person("parent") child1 = Person("child1") child2 = Person("child2") parent.hasChild = [child1, child2] AllDifferent([child1, child2])# debug=0 : empeche owlready2 d'echoer la commande Java (classpath machine). sync_reasoner_hermit(world2, debug=0) bigfamily_dl = BigFamily in parent.is_aprint(f"HermiT (DL) : parent classifie BigFamily ? {bigfamily_dl}")exceptExceptionas e:print(f"HermiT (DL) indisponible ({type(e).__name__}).")else:print("OWLReady2 non disponible : partie DL ignoree.")# RL : meme KG en rdflib + owlrlEXF = Namespace("http://test.org/family#")g_card = Graph()bn2 = BNode()g_card.add((bn2, RDF.type, OWL.Restriction))g_card.add((bn2, OWL.onProperty, EXF.hasChild))g_card.add((bn2, OWL.minQualifiedCardinality, Literal(2, datatype=XSD.nonNegativeInteger)))g_card.add((bn2, OWL.onClass, EXF.Person))g_card.add((EXF.BigFamily, OWL.equivalentClass, bn2))g_card.add((EXF.parent, RDF.type, EXF.Person))g_card.add((EXF.child1, RDF.type, EXF.Person))g_card.add((EXF.child2, RDF.type, EXF.Person))g_card.add((EXF.parent, EXF.hasChild, EXF.child1))g_card.add((EXF.parent, EXF.hasChild, EXF.child2))g_card.add((EXF.child1, OWL.differentFrom, EXF.child2))owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_card)bigfamily_rl = (EXF.parent, RDF.type, EXF.BigFamily) in g_cardprint(f"owlrl (RL) : parent classifie BigFamily ? {bigfamily_rl}")print("\n| Profil | Classification parent -> BigFamily (min 2) ? |")print("|--------------|-----------------------------------------------|")print(f"| RL (owlrl) | {bigfamily_rl}")print(f"| DL (HermiT) | {bigfamily_dl if bigfamily_dl isnotNoneelse'non execute'}")print("\n=> Ici la difference est nette : seule la logique DL (HermiT) classifie ""par cardinalite minimale ;")print(" le profil RL ne supporte pas min-cardinality en position de super-classe.")
HermiT (DL) : pizza1 classifiee comme MeatPizza ? True
owlrl (RL) : pizza1 classifiee comme MeatPizza ? True
| Profil | Classification automatique pizza1 -> MeatPizza ? |
|--------------|---------------------------------------------------|
| RL (owlrl) | True
| DL (HermiT) | True
Note : contrairement a une idee repandue, owlrl implemente la regle cls-svf1 d'OWL 2 RL,
ce qui lui permet ICI de deriver la classification via someValuesFrom (equivalentClass + restriction).
La difference RL/DL se manifeste sur des constructions plus riches (cardinalites, unions, classification complete)
que le profil RL ne couvre pas et que seul un raisonneur DL comme HermiT traite integralement.
============================================================
Contraste reel : BigFamily = (hasChild min 2)
============================================================
HermiT (DL) : parent classifie BigFamily ? True
owlrl (RL) : parent classifie BigFamily ? False
| Profil | Classification parent -> BigFamily (min 2) ? |
|--------------|-----------------------------------------------|
| RL (owlrl) | False
| DL (HermiT) | True
=> Ici la difference est nette : seule la logique DL (HermiT) classifie par cardinalite minimale ;
le profil RL ne supporte pas min-cardinality en position de super-classe.
Lecture de l’exemple 6 : realization, et soundness vs completeness
L’exemple 6 illustre en acte deux concepts théoriques centraux que les raisonneurs mettent en œuvre — et qu’il faut nommer explicitement.
Realization (la 4ᵉ tâche canonique). Un raisonneur OWL accomplit traditionnellement quatre tâches : (1) consistency checking (l’ontologie est-elle cohérente ?), (2) entailment (une formule est-elle conséquence ?), (3) classification (calculer la hiérarchie des classes), et (4) realization — déterminer, pour un individu donné, la classe la plus spécifique à laquelle il appartient. C’est exactement ce qui se produit ici : HermiT a réalisépizza1 et conclu qu’elle n’est pas seulement une Pizza mais la classe plus précise MeatPizza (on le voit dans le journal Reparenting pizza2.pizza1: {Pizza} => {MeatPizza}, et l’on récupère explicitement la classe réalisée via pizza1.is_a). La realization est le dual de la classification : la classification range les classes entre elles, la realization range chaque individu dans sa classe la plus fine.
Soundness vs completeness. L’exemple 6 sépare aussi, en acte, deux propriétés logiques d’un raisonneur, qu’il faut distinguer de la simple tractabilité (déjà évoquée : OWL 2 RL reste en temps polynomial).
Un raisonneur est sound (correct) si toute inférence qu’il produit est effectivement une conséquence logique — il n’invente jamais de triplet faux.
Il est complete (complet) s’il produit toutes les conséquences logiques — il n’en oublie aucune.
Le profil OWL 2 RL (owlrl, reasonable) est sound mais nécessairement incomplet vis-à-vis d’OWL 2 DL : pour rester polynomial, il renonce délibérément à certaines constructions (dont la minCardinality en position de super-classe). C’est pourquoi owlrl rate ici la classification parent → BigFamily (min 2) — ce n’est pas un bug, c’est une incomplétude garantie par construction, le prix de la tractabilité. HermiT (DL, NExpTime-complet) est complet pour OWL 2 DL et la dérive. À l’inverse, sur someValuesFrom (la première partie de l’exemple), owlrl réussit la classification car cette construction-là est couverte par RL.
Sound (correct)
Complete (complet)
Coût
OWL 2 RL (owlrl, reasonable)
✅
❌ (par construction)
polynomial
OWL 2 DL (HermiT)
✅
✅ (pour OWL 2 DL)
NExpTime-complet
À retenir. Quand un raisonneur RL « rate » une inférence qu’un raisonneur DL trouve, ce n’est pas une défaillance d’implémentation : c’est le compromis expressivité/tractabilité formalisé par les profils OWL 2. Choisir un profil, c’est choisir quelle part d’incomplétude on accepte contre la garantie d’un temps polynomial.
Exercices à compléter
Les exercices ci-dessous reprennent les concepts illustrés par les exemples guidés ci-dessus. Chaque exercice contient un squelette de code à compléter.
Chaque squelette reprend le scénario d’un exemple guidé, avec la structure et les affichages attendus déjà en place : compléter l’exercice revient à retrouver la démarche de l’exemple, pas à deviner sa forme. Le notebook s’exécute de bout en bout même les exercices laissés vides — les cellules stub n’interrompent jamais la chaîne d’exécution.
Exercice 1 : Benchmark sur une ontologie plus grande
Téléchargez la Pizza Ontology complète et comparez les temps d’exécution des raisonneurs disponibles.
Consignes : - Charger l’ontologie depuis l’URL fournie avec rdflib.Graph.parse() - Appliquer chaque raisonneur disponible sur une copie du graphe - Mesurer le temps avec time.time() et afficher un tableau comparatif
Hint : L’URL de référence est https://protege.stanford.edu/ontologies/pizza/pizza.owl (format XML). Utilisez copy.deepcopy() ou reconstituez le graphe à chaque itération pour éviter les biais.
# Exercice 1 : Benchmark sur la Pizza Ontology complete# --------------------------------------------------------# Chargez l'ontologie pizza, mesurez le temps de chaque raisonneur# et affichez un tableau comparatif.## Hint : rdflib.Graph().parse("data/pizza.owl", format='xml') # copie locale vendoree (reproductible)# Hint : copiez le graphe avant chaque raisonnement (serialisez en nt puis re-parsez)import timeimport copyimport osPIZZA_LOCAL ="data/pizza.owl"# copie vendoree ; fallback url possible# TODO : charger l'ontologie pizza depuis la copie locale# g_pizza = ...# TODO : mesurer le temps pour owlrl# g_owlrl_pizza = ...# start = time.time()# ...# t_owlrl_pizza = time.time() - start# TODO : mesurer le temps pour reasonable (si disponible)# TODO : afficher un tableau comparatif# print(f"{'Raisonneur':<20} {'Temps (s)':<12} {'Triples inferes':<20}")pass# TODO: completez cet exerciceprint("Exercice a completer")
Exercice a completer
Indice : Exercice 1
Pour charger l’ontologie pizza depuis l’URL, utilisez :
Pour copier le graphe avant chaque raisonnement, vous pouvez soit : - Recréer un nouveau Graph et re-parser l’URL - Utiliser copy.deepcopy(g_pizza) (import copy)
Attention : la Pizza Ontology complète est beaucoup plus grande (~2000 triples). Les temps de raisonnement seront significativement plus longs.
Exercice 2 : Vérification de l’équivalence des résultats RL
Les raisonneurs OWL 2 RL (owlrl, reasonable) devraient produire le même ensemble de triples. Vérifiez si c’est le cas.
Consignes : - Appliquer owlrl et reasonable sur le même graphe de départ - Calculer la différence symétrique entre les deux ensembles de triples inférés - Analyser et expliquer les écarts éventuels
# Exercice 2 : Verification de l'equivalence owlrl vs reasonable# Comparez les triples inferes par les deux raisonneurs OWL 2 RL.## Hint : set(graphe) pour convertir en ensemble de triples# Hint : difference symetrique avec A.symmetric_difference(B)## Etape 1 : reconstruire g_owlrl et g_reasonable depuis g, le graphe de base# Etape 2 : appliquer chaque raisonneur# Etape 3 : calculer les ensembles de triples inferes par difference avec le graphe de base# Etape 4 : afficher les triples uniquement owlrl, uniquement reasonable, et la difference symetriquepass# TODO: completez cet exerciceprint("Exercice a completer")
Exercice a completer
Indice : Exercice 2
Pour calculer la différence symétrique entre deux ensembles de triples :
Pour analyser les écarts, affichez quelques triples de chaque différence :
print("Uniquement owlrl:")for t inlist(inferred_owlrl - inferred_reasonable)[:5]:print(f" {t}")
Les différences s’expliquent par les règles optionnelles implémentées par owlrl mais pas par reasonable (ex: reflexivité subClassOf, typage XSD complet).
Exercice 3 : Profilage avancé avec cProfile
Identifiez les goulots d’étranglement du raisonnement owlrl avec cProfile.
Consignes : - Profiler l’appel owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g) sur le graphe pizza - Extraire et afficher les 10 fonctions les plus coûteuses (tri par temps cumulé) - Comparer avec le profil de reasonable si disponible
# Exercice 3 : Profilage avance avec cProfile# Identifiez les fonctions les plus couteuses dans le raisonnement owlrl.## Hint : cProfile.Profile pour capturer le profil# Hint : pstats.Stats trie par 'cumulative' puis print_stats des 10 premieresimport cProfileimport pstatsimport io# Etape 1 : reconstruire une copie du graphe de base# Etape 2 : profiler le raisonnement owlrl (enable, expand, disable)# Etape 3 : afficher les 10 fonctions les plus couteuses, tri cumulatifpass# TODO: completez cet exerciceprint("Exercice a completer")
Les fonctions les plus coûteuses seront probablement dans le module owlrl (ex: DeductiveClosure.expand, _rule_OWLRL_SubClassOf).
Exercice 4 : Detection d’incoherence avec un raisonneur DL (HermiT)
Une force du raisonnement OWL 2 DL est la detection automatique d’incoherences dans une base de connaissances. Construisez une ontologie avec une contradiction explicite et faites-la detecter par HermiT.
Consignes : 1. Avec OWLReady2, declarer deux classes disjointes : Vegetal et Animal (AllDisjoint([Vegetal, Animal])) 2. Créer une instance tomate declaree comme a la foisVegetal ET Animal (incoherence explicite) 3. Lancer sync_reasoner_hermit() dans un bloc try / except OwlReadyInconsistentOntologyError 4. Verifier qu’owlrl (profil RL) ne detecte pas cette incoherence avec la même rigueur (le profil RL n’inclut pas la classification DL complete) 5. Conclure sur l’usage : DL pour la validation logique, RL pour la materialisation rapide
Indices : - from owlready2 import get_ontology, Thing, AllDisjoint, sync_reasoner_hermit, OwlReadyInconsistentOntologyError - onto = get_ontology("http://test.org/disjoint.owl") puis with onto: class Vegetal(Thing): pass - L’incoherence se materialise par l’exception OwlReadyInconsistentOntologyError au moment du sync_reasoner_hermit() - Pour owlrl, l’incoherence n’est pas levee comme exception ; il faut interroger le graphe et chercher les inferences contradictoires manuellement - Question pedagogique : pourquoi le profil RL accepte-t-il des incoherences que DL rejette ?
# Exercice 4 : Detection d'incoherence avec HermiT (DL)# TODO etudiant : declarer Vegetal et Animal disjoints avec OWLReady2# from owlready2 import get_ontology, Thing, AllDisjoint# from owlready2 import sync_reasoner_hermit, OwlReadyInconsistentOntologyError# onto = get_ontology("http://test.org/disjoint.owl")# with onto:# class Vegetal(Thing): pass# class Animal(Thing): pass# AllDisjoint([Vegetal, Animal])# TODO etudiant : creer une instance contradictoire (a la fois Vegetal ET Animal)# with onto:# tomate = Vegetal("tomate")# tomate.is_a.append(Animal)# TODO etudiant : lancer HermiT dans un try/except# try:# with onto:# sync_reasoner_hermit()# print("OK : aucune incoherence detectee")# except OwlReadyInconsistentOntologyError as e:# print(f"INCOHERENCE detectee par HermiT : {e}")# TODO etudiant : comparer avec owlrl (profil RL)# Indice : reconstruire le meme KG en pur rdflib avec owl:disjointWith,# appliquer owlrl.DeductiveClosure(OWLRL_Semantics).expand(g),# verifier que le graphe se materialise SANS exception (RL est plus tolerant)print("Exercice a completer : detection incoherence HermiT vs tolerance owlrl")
Exercice a completer : detection incoherence HermiT vs tolerance owlrl
Exercice 5 : Inferences sur axiomes de proprietes OWL
OWL fournit des axiomes puissants sur les proprietes : TransitiveProperty, SymmetricProperty, InverseProperty. Le raisonneur materialise les triples implicites en appliquant ces axiomes. Construisez un mini-KG et verifiez les inferences attendues.
Consignes : 1. Construire un graphe g_props avec trois proprietes typees : - :hasAncestor declaree owl:TransitiveProperty ; ajouter (A hasAncestor B) et (B hasAncestor C) — on attend (A hasAncestor C) inferre - :knows declaree owl:SymmetricProperty ; ajouter (A knows B) — on attend (B knows A) inferre - :hasParent declaree owl:inverseOf :hasChild ; ajouter (A hasParent B) — on attend (B hasChild A) inferre 2. Appliquer owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_props) 3. Verifier chacune des 3 inferences attendues avec une requête SPARQL ASK 4. Compter len(g_props_avant) vs len(g_props_apres) pour quantifier la materialisation 5. Bonus : ajouter owl:ReflexiveProperty ou owl:IrreflexiveProperty et observer leur impact (ou son absence en profil RL)
Indices : - from rdflib import Graph, Namespace, RDF, OWL ; EX = Namespace("http://test.org/") - Declarer une propriete transitive : g.add((EX.hasAncestor, RDF.type, OWL.TransitiveProperty)) - ASK { :A :hasAncestor :C } retourne True si l’inference a eu lieu - Le nombre de triples doit croitre : c’est la materialisation de la cloture - Question pedagogique : combien de hops transitifs owlrl materialise-t-il sur une chaîne de 10 éléments ?
# Exercice 5 : Inferences sur axiomes de proprietes OWL# TODO etudiant : construire le graphe avec 3 proprietes typees# from rdflib import Graph, Namespace, RDF, OWL, URIRef# import owlrl# EX = Namespace("http://test.org/")# g_props = Graph()# g_props.bind("ex", EX); g_props.bind("owl", OWL)# TODO etudiant : declarer hasAncestor TransitiveProperty + chaine A -> B -> C# g_props.add((EX.hasAncestor, RDF.type, OWL.TransitiveProperty))# g_props.add((EX.A, EX.hasAncestor, EX.B))# g_props.add((EX.B, EX.hasAncestor, EX.C))# TODO etudiant : declarer knows SymmetricProperty + (A knows B)# g_props.add((EX.knows, RDF.type, OWL.SymmetricProperty))# g_props.add((EX.A, EX.knows, EX.B))# TODO etudiant : declarer hasParent inverseOf hasChild + (A hasParent B)# g_props.add((EX.hasParent, OWL.inverseOf, EX.hasChild))# g_props.add((EX.A, EX.hasParent, EX.B))# TODO etudiant : appliquer owlrl, compter triples avant/apres# n_avant = len(g_props)# owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_props)# n_apres = len(g_props)# print(f"Triples avant: {n_avant}, apres: {n_apres}, materialises: {n_apres - n_avant}")# TODO etudiant : verifier les 3 inferences attendues via SPARQL ASK# r1 = g_props.query(f"ASK {{ <{EX.A}> <{EX.hasAncestor}> <{EX.C}> }}")# r2 = g_props.query(f"ASK {{ <{EX.B}> <{EX.knows}> <{EX.A}> }}")# r3 = g_props.query(f"ASK {{ <{EX.B}> <{EX.hasChild}> <{EX.A}> }}")# print(f"Transitive A->C : {bool(r1)}, Symetrique B->A : {bool(r2)}, Inverse B->A : {bool(r3)}")print("Exercice a completer : axiomes TransitiveProperty + SymmetricProperty + inverseOf")
Exercice a completer : axiomes TransitiveProperty + SymmetricProperty + inverseOf
Les profils OWL 2 RL (owlrl, reasonable) et OWL 2 DL (HermiT) différent en expressivite. Une différence cle réside dans la portée des restrictions de classe couvertes : explorez d’abord someValuesFrom, puis minCardinality, pour localiser où le profil RL atteint sa limite. Verifiez empiriquement cette différence.
Consignes : 1. Construire une ontologie avec : - Classe MeatPizza définie comme equivalente a Pizza et (hasTopping some MeatTopping) - Instance pizza1 declaree Pizza avec pizza1 hasTopping topping1 ou topping1 a MeatTopping 2. Appliquer HermiT (via OWLReady2) : pizza1 doit etre automatiquement classifiee comme MeatPizza 3. Appliquer owlrl (profil RL) sur le même KG transcrit en rdflib : contrairement à une idée répandue, owlrl supporte la classification someValuesFrom (via la règle cls-svf1 d’OWL 2 RL) — c’est sur la minCardinality (cf. BigFamily = hasChild min 2) que le profil RL montre sa limite 4. Conclure : RL suffit pour la classification someValuesFrom et la materialisation rapide de hiérarchies subClassOf + propertyChainAxiom ; DL reste nécessaire pour les constructions hors-profil-RL (cardinalités minimales, unions complexes)
Indices : - OWLReady2 : class MeatPizza(Pizza): equivalent_to = [Pizza & hasTopping.some(MeatTopping)] - Après sync_reasoner_hermit(), verifier MeatPizza in pizza1.is_a ou pizza1 in MeatPizza.instances() - En rdflib + owlrl : declarer MeatPizza owl:equivalentClass [a owl:Restriction ; owl:onProperty :hasTopping ; owl:someValuesFrom :MeatTopping] puis verifier la présence de l’inference pizza1 a MeatPizza après expansion (règle cls-svf1) ; comparer avec la cardinalité min 2 (BigFamily) que seul HermiT dérive - Pour rester clair pedagogiquement : afficher un mini-tableau avec RL infere classification ? vs DL infere classification ? - Question pedagogique : quel(s) profil(s) OWL 2 supporte(nt) someValuesFrom dans les axiomes d’equivalence ?
# Exercice 6 : OWL 2 RL vs OWL 2 DL sur someValuesFrom (classification automatique)# TODO etudiant : construire l'ontologie pizza avec OWLReady2 (DL)# from owlready2 import get_ontology, Thing, ObjectProperty, sync_reasoner_hermit# onto = get_ontology("http://test.org/pizza.owl")# with onto:# class Pizza(Thing): pass# class Topping(Thing): pass# class MeatTopping(Topping): pass# class hasTopping(ObjectProperty):# domain = [Pizza]; range = [Topping]# class MeatPizza(Pizza):# equivalent_to = [Pizza & hasTopping.some(MeatTopping)]# topping1 = MeatTopping("topping1")# pizza1 = Pizza("pizza1")# pizza1.hasTopping.append(topping1)# TODO etudiant : lancer HermiT et verifier la classification# with onto:# sync_reasoner_hermit()# print(f"pizza1.is_a apres HermiT : {pizza1.is_a}")# print(f"pizza1 classifiee comme MeatPizza ? {MeatPizza in pizza1.is_a}")# TODO etudiant : transcrire le meme KG en pur rdflib + owlrl# from rdflib import Graph, Namespace, RDF, OWL, BNode# import owlrl# EX = Namespace("http://test.org/")# g_rl = Graph()# Indice : declarer la restriction comme BNode# bn = BNode()# g_rl.add((bn, RDF.type, OWL.Restriction))# g_rl.add((bn, OWL.onProperty, EX.hasTopping))# g_rl.add((bn, OWL.someValuesFrom, EX.MeatTopping))# g_rl.add((EX.MeatPizza, OWL.equivalentClass, bn))# ... ajouter les autres triples (pizza1, topping1)# owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_rl)# verifier (pizza1, RDF.type, MeatPizza) absent du graphe materialisee# TODO etudiant : afficher tableau comparatif RL vs DL sur someValuesFrom# print("| Profil | Classification automatique pizza1 -> MeatPizza ? |")# print(f"| RL (owlrl) | {bool((EX.pizza1, RDF.type, EX.MeatPizza) in g_rl)} |")# print(f"| DL (HermiT) | {MeatPizza in pizza1.is_a} |")print("Exercice a completer : someValuesFrom inference RL (non) vs DL (oui)")
Exercice a completer : someValuesFrom inference RL (non) vs DL (oui)
Conclusion
Résumé des compromis
Critère
owlrl
HermiT (OWLReady2)
reasonable
Growl
Temps (ontologie de test)
~60 ms
~1,4 s (JVM incluse)
~1,5 ms
non mesuré (absent)
Triplets inférés (RL)
211
N/A (non matérialisés)
42
N/A
Expressivité
OWL 2 RL
OWL 2 DL complet
OWL 2 RL
OWL 2 RL (76/78 règles prouvées)
Détection d’incohérence
Non
Oui (exemple 4)
Non
(non testé)
Intégration Python
Native RDFLib
Bridge Java
Native PyO3
CLI subprocess
Facilité d’installation
Très haute
Moyenne (Java requis)
Haute
Faible (compilation C)
Ces compromis ne sont pas des slogans : ils ont été mesurés dans ce notebook. Sur le profil RL, reasonable combine ~1,5 ms et l’essentiel des inférences utiles — les classifications métier testées (exemples 2 et 6) sont identiques à celles d’owlrl, qui offre en revanche la matérialisation la plus exhaustive (211 triplets, règles optionnelles comprises) pour ~60 ms ; à l’échelle de l’exemple 1, l’écart interprété/compilé atteint ~72x (2,97 s contre 0,041 s). HermiT, hors course sur le temps, est le seul à rendre les services DL : incohérence (exemple 4) et cardinalité qualifiée (exemple 6, hasChild min 2). Et l’exemple 5 a montré le coût caché de la matérialisation : sept triplets d’axiomes de propriétés deviennent 126 après fermeture — raisonner enrichit le graphe autant qu’il l’épaissit. La question « pepperoni est-elle une VegetarianPizza ? » de la section liminaire résume à elle seule l’enjeu : sans triplet de disjonction, aucun de ces raisonneurs — rapide ou complet — ne répondra « non » ; c’est la modélisation qui décide de ce que le raisonnement peut prouver.
Recommandations
Prototype rapide: owlrl (pas d’installation, intégré à RDFLib)
Production Python: reasonable (performance Rust, installation simple)
OWL 2 DL complet: HermiT via OWLReady2
Systèmes critiques: Growl (vérification formelle)
Références savantes
OWL 2 Profiles — Motik, Cuenca Grau, Horrocks et al. (Recommandation W3C, 27 octobre 2009). Définition formelle des profils OWL 2 DL/RL/EL/QL et de leurs compromis complexité.
OWL 2 Direct Semantics — Motik, Patel-Schneider, Cuenca Grau (Recommandation W3C, 11 décembre 2012). Sémantique formelle d’OWL 2 implémentée par les raisonneurs DL (HermiT).
The Description Logic Handbook — Baader, Calvanese, McGuinness, Nardi, Patel-Schneider (eds.), Cambridge University Press, 2e éd. 2007. Référence fondatrice des logiques de description sous-jacentes à OWL.
HermiT: An OWL 2 Reasoner — Glimm, Horrocks, Motik, Stoilos, Wang, Journal of Automated Reasoning 53(4), 2014. Description du raisonneur et de l’algorithme hypertableau.
OWL pizzas — Rector, Drummond, Stevens et al., OWL pizzas: Practical expérience of teaching OWL-DL, EKAW 2004. Origine et analyse pédagogique de la Pizza Ontology utilisée comme ontologie de test.
Ce notebook a compare quatre raisonneurs OWL couvrant un spectre allant de l’implementation Python pure au code compile en Rust et en C, avec des profils OWL allant de RL (polynomial) a DL (NExpTime-complet). Les benchmarks sur l’ontologie pizza ont revele des ecarts de performance considerables : reasonable (Rust) est environ 70 fois plus rapide qu’owlrl (Python) sur l’ontologie pizza complete (et ~40 fois sur l’ontologie de test reduite a 40 triples), tandis qu’HermiT (Java, OWL 2 DL complet) offre une expressivite superieure au prix d’un temps d’exécution plus eleve.
La différence majeure entre les profils OWL 2 RL et OWL 2 DL a ete illustree par les exercices : le profil RL (owlrl, reasonable) materialise efficacement les hiérarchies de classes, les proprietes transitives et les inferences de base, et couvre la classification someValuesFrom via la règle cls-svf1 d’OWL 2 RL (cf. exemple 6 : pizza1 est bien classifiée MeatPizza par owlrl). Sa limite réelle se manifeste sur des constructions DL plus riches comme la minCardinality en position de super-classe, que seul un raisonneur DL (HermiT) traite intégralement. Le profil DL (HermiT) detecte les incoherences ontologiques et classifie les instances selon des règles complexes, ce qui est indispensable dans les domaines critiques (medecine, industrie). Le choix du raisonneur depend donc du compromis entre expressivite requise et performances acceptables.
Perspectives concrètes. En production, le profil se choisit par la fréquence d’exécution et le coût de l’erreur. Pour une matérialisation relancée à chaque ingestion de triplets (pipeline RDF, index de GraphRAG à la SW-12), la vitesse de reasonable fait la différence ; pour la validation à froid d’une ontologie avant publication — disjonctions, cardinalités, cohérence globale — c’est un raisonneur DL comme HermiT qui s’impose, en complément d’une validation SHACL (SW-8) qui vérifie la forme des données là où le raisonneur déduit leur sens. Deux pistes hors périmètre de ce notebook : le profil OWL 2 EL, quasi linéaire pour classifier de très grandes hiérarchies, et le notebook SW-14 (coup ontologique), qui compose précisément ces briques — OWL, SHACL, OWL-RL et RDF-star — autour d’un même geste d’extension de vocabulaire.
Ce notebook conclut la serie Semantic Web. Au fil des notebooks, vous avez parcouru l’ensemble de la pile technologique : les triplets RDF (SW-1 a SW-3), les requêtes SPARQL (SW-4 a SW-5), les schemas et ontologies RDFS/OWL (SW-6 a SW-7), la validation SHACL (SW-8), les formats modernes JSON-LD et RDF-Star (SW-9 a SW-10), les graphes de connaissances (SW-11), l’integration avec les LLMs via GraphRAG (SW-12), et enfin les moteurs d’inférence (SW-13).