SW-14 — Le coup ontologique comme diff de graphe exécutable
Concept : l’extension du vocabulaire η : L_t → L_{t+1}, incarnée comme un diff de graphe : un ensemble de triplets ajoutés, un verdict SHACL, un delta d’inférences owlrl, et une trace d’auteur RDF-star.
Outils canoniques : - OWL (via rdflib) — déclare la nouvelle classe/relation/contrainte - SHACL (via pyshacl) — décide si l’extension est admissible - OWL-RL (via owlrl) — calcule les nouvelles conséquences - RDF-star (rdflib 7+) — garde la provenance de qui a proposé quoi
Pourquoi ce notebook existe : la série SemanticWeb porte déjà séparément SW-7 OWL, SW-8 SHACL, SW-10 RDF-star, SW-13 Reasoners. Ce qui manquait, c’est le notebook qui les compose autour d’un même geste.
On travaille sur une petite ontologie d’exemple (ex:) qui modélise une bibliothèque : Book (avec title, author), Member (avec name), Loan (qui relie un Member à un Book). La classe Book est disjointe de Member (un livre n’est pas un membre).
L’extension η ajoute : une nouvelle sous-classe Ebook de Book, une nouvelle propriété format pour Ebook, et une assertion de provenance RDF-star sur l’une des nouvelles triples.
Exemple résolu 1 — Proposer une extension
Tâche : prendre l’ontologie ci-dessous (vide pour vous), y ajouter :
une nouvelle classe Ebook (sous-classe de Book)
une nouvelle datatype property format (domain = Ebook, range = xsd:string)
une assertion ex:ebook1 rdf:type ex:Ebooketex:ebook1 ex:format "PDF"
Puis afficher le diff : t_added = g_extension - g_base (la liste des triplets effectivement ajoutés, en notation Turtle).
# Solution complete — Exemple résolu 1from rdflib import Graph, Namespace, URIRef, Literal, BNodefrom rdflib.namespace import RDF, RDFS, OWL, XSDEX = Namespace("http://example.org/library#")# --- Etat initial L_t (ontologie de base) ---g_base = Graph()g_base.bind("ex", EX)g_base.bind("owl", OWL)g_base.bind("rdfs", RDFS)g_base.bind("xsd", XSD)# Classesfor c in ["Book", "Member", "Loan", "Library", "Person"]: g_base.add((EX[c], RDF.type, OWL.Class))# Proprietesg_base.add((EX.title, RDF.type, OWL.DatatypeProperty))g_base.add((EX.title, RDFS.domain, EX.Book))g_base.add((EX.title, RDFS.range, XSD.string))g_base.add((EX.author, RDF.type, OWL.DatatypeProperty))g_base.add((EX.author, RDFS.domain, EX.Book))g_base.add((EX.author, RDFS.range, XSD.string))g_base.add((EX.name, RDF.type, OWL.DatatypeProperty))g_base.add((EX.name, RDFS.domain, EX.Member))g_base.add((EX.name, RDFS.range, XSD.string))g_base.add((EX.borrows, RDF.type, OWL.ObjectProperty))g_base.add((EX.borrows, RDFS.domain, EX.Member))g_base.add((EX.borrows, RDFS.range, EX.Book))# Disjonction Book vs Memberg_base.add((EX.Book, OWL.disjointWith, EX.Member))g_base.add((EX.Member, OWL.disjointWith, EX.Book))print(f"Etat initial L_t : {len(g_base)} triplets")# --- L'extension : le coup eta : L_t -> L_{{t+1}} ---g_ext = Graph()for t in g_base: g_ext.add(t)# 1. Nouvelle classe Ebookg_ext.add((EX.Ebook, RDF.type, OWL.Class))g_ext.add((EX.Ebook, RDFS.subClassOf, EX.Book))# 2. Nouvelle propriete formatg_ext.add((EX.fmt, RDF.type, OWL.DatatypeProperty))g_ext.add((EX.fmt, RDFS.domain, EX.Ebook))g_ext.add((EX.fmt, RDFS.range, XSD.string))# 3. Assertionsg_ext.add((EX.ebook1, RDF.type, EX.Ebook))g_ext.add((EX.ebook1, EX.fmt, Literal("PDF")))# --- Le diff de triplets ---diff_set =set(g_ext) -set(g_base)print(f"\nL'extension ajoute {len(diff_set)} triplets :")print("-"*60)diff_list =sorted(diff_set, key=lambda t: (str(t[2]), str(t[1]), str(t[0])))for s, p, o in diff_list: s_str = s.n3(g_ext.namespace_manager) p_str = p.n3(g_ext.namespace_manager) o_str = o.n3(g_ext.namespace_manager)print(f" {s_str}{p_str}{o_str} .")
format rdf:type owl:DatatypeProperty + format rdfs:domain ex Ebook + format rdfs:range xsd:string
OWL
Instancier
ebook1 rdf:type ex:Ebook + ebook1 ex:format "PDF"
RDF
Le diff de triplets est ce qui sépare L_{t+1} de L_t. C’est l’objet qu’on peut rejouer, valider, inférer, ou refuser.
Exemple résolu 2 — L’admissibilité (SHACL)
Tâche : 1. Construire un shape SHACL minimal pour Ebook (cardinalité >=1 sur format). 2. Valider l’extension g_ext (devrait être conforme). 3. Construire une extension rejetée : ebook2 rdf:type ex:Ebook SANS format (viole la cardinalité). 4. Valider l’extension rejetée — doit produire un rapport de violation.
# Solution complete — Exemple résolu 2from pyshacl import validatefrom rdflib import Graph, Namespace, URIRef, Literalfrom rdflib.namespace import RDF, RDFS, XSD, SHEX = Namespace("http://example.org/library#")# --- Le shape SHACL ---g_shapes = Graph()g_shapes.bind("ex", EX)g_shapes.bind("sh", SH)EbookShape = EX.EbookShapeg_shapes.add((EbookShape, RDF.type, SH.NodeShape))g_shapes.add((EbookShape, SH.targetClass, EX.Ebook))g_shapes.add((EbookShape, SH.property, EX.EbookShape_format))g_shapes.add((EX.EbookShape_format, SH.path, EX.fmt))g_shapes.add((EX.EbookShape_format, SH.minCount, Literal(1)))# --- CAS A : extension conforme ---conforms_a, report_g_a, report_text_a = validate(g_ext, shacl_graph=g_shapes, inference='none', debug=False, serialize_report=False)print("="*60)print("CAS A — l'extension conforme (g_ext avec ebook1.format = 'PDF')")print("="*60)print(f" conforms : {conforms_a}")print(f" report_g : type={type(report_g_a).__name__}")report_text_head = report_text_a.splitlines()[0] if report_text_a else'<vide>'print(f" text head : {report_text_head[:120]}")# --- CAS B : extension rejetee ---g_rejected = Graph()for t in g_base: g_rejected.add(t)g_rejected.add((EX.Ebook, RDF.type, OWL.Class))g_rejected.add((EX.Ebook, RDFS.subClassOf, EX.Book))g_rejected.add((EX.fmt, RDF.type, OWL.DatatypeProperty))g_rejected.add((EX.fmt, RDFS.domain, EX.Ebook))g_rejected.add((EX.fmt, RDFS.range, XSD.string))g_rejected.add((EX.ebook2, RDF.type, EX.Ebook))conforms_b, report_g_b, report_text_b = validate(g_rejected, shacl_graph=g_shapes, inference='none', debug=False, serialize_report=False)print()print("="*60)print("CAS B — l'extension rejetee (ebook2 sans format)")print("="*60)print(f" conforms : {conforms_b}")print(f" report_g : type={type(report_g_b).__name__}")# Detail des violationsifnot conforms_b:print(" - detail des violations :") n_viol =0for s, p, o in report_g_b.triples((None, SH.result, None)):for s2, p2, o2 in report_g_b.triples((o, None, None)):if p2 == SH.focusNode: n_viol +=1print(f" violation #{n_viol}: focusNode={o2.n3()}, path=ex:fmt")breakif n_viol >=3:break
============================================================
CAS A — l'extension conforme (g_ext avec ebook1.format = 'PDF')
============================================================
conforms : True
report_g : type=Graph
text head : Validation Report
============================================================
CAS B — l'extension rejetee (ebook2 sans format)
============================================================
conforms : False
report_g : type=Graph
- detail des violations :
violation #1: focusNode=<http://example.org/library#ebook2>, path=ex:fmt
Interprétation — l’admissibilité démontrée par les deux cas
Sans le cas rejeté, l’admissibilité n’est pas démontrée — un shape qui n’attrape jamais rien n’est pas un shape, c’est un commentaire. Le cas B prouve que la contrainte sh:minCount 1 sur ex:format est effectivement vérifiée, pas seulement déclarée.
L’admissibilité SHACL est ce qui distingue « on ajoute des triplets » de « on ajoute une extension cohérente ».
Exemple résolu 3 — Le delta d’inférences (OWL-RL)
Tâche : 1. Calculer les inférences OWL-RL sur l’état initialg_base -> g_base_inferred. 2. Calculer les inférences OWL-RL sur l’étendug_ext -> g_ext_inferred. 3. Énumérer (pas seulement compter) le delta : delta_inferences = g_ext_inferred - g_base_inferred. 4. Vérifier qu’une extension qui ne produit aucun nouveau triplet inféré est un coup nul — construire un tel cas et le démontrer.
# Solution complete — Exemple résolu 3from rdflib import Graph, Namespace, URIRef, Literalfrom rdflib.namespace import RDF, RDFS, OWL, XSDimport owlrlEX = Namespace("http://example.org/library#")# --- Inference sur g_base (etat initial) ---g_base_inferred = Graph()for t in g_base: g_base_inferred.add(t)owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_base_inferred)inf_base =set(g_base_inferred)print(f"Etat initial L_t infere : {len(inf_base)} triplets")# --- Inference sur g_ext (apres extension) ---g_ext_inferred = Graph()for t in g_ext: g_ext_inferred.add(t)owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_ext_inferred)inf_ext =set(g_ext_inferred)print(f"Etat etendu L_{{t+1}} infere : {len(inf_ext)} triplets")# --- DELTA D'INFERENCES ---delta_inf = inf_ext - inf_baseprint(f"\nDelta d'inferences : {len(delta_inf)} nouveaux triplets inferes")print("-"*60)delta_list =sorted(delta_inf, key=lambda t: (str(t[0]), str(t[1]), str(t[2])))for s, p, o in delta_list[:15]:print(f" {s.n3()}{p.n3()}{o.n3()} .")iflen(delta_list) >15:print(f" ... et {len(delta_list) -15} de plus.")# --- CAS DU COUP NUL ---g_nul = Graph()for t in g_base: g_nul.add(t)g_nul.add((EX.orphan, RDF.type, EX.Book)) # instance isolee d'une classe existanteg_nul_inferred = Graph()for t in g_nul: g_nul_inferred.add(t)owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g_nul_inferred)inf_nul =set(g_nul_inferred)delta_nul = inf_nul - inf_baseprint()print("="*60)print("CAS DU COUP NUL : ajouter une instance isolee d'une classe deja existante")print("="*60)print(f" delta d'inferences : {len(delta_nul)} triplets")iflen(delta_nul) ==0:print(" -> Coup nul : l'extension n'apporte aucune nouvelle consequence.")print(" Verdict : eta est admissible mais infructueux.")
Le delta d’inférences est l’effet utile du coup ontologique. Une extension qui n’en produit aucun n’est pas « sans intérêt » — c’est un signal : - soit η est trop conservatif (n’ajoute rien qui se propage), - soit l’ontologie initiale est déjà saturée sur cette zone, - soit le geste est mal formulé (vocabulaire sans accroche inférentielle).
C’est une mesure falsifiable de la productivité du coup, pas un échec technique.
Exemple résolu 4 — La provenance RDF-star
Tâche : 1. Encapsuler au moins un des triplets ajoutés dans une triple RDF-star qui porte la provenance (« ce triplet a été proposé par X, à la date Y »). 2. Interroger la provenance via SPARQL : demander « quels triplets l’agent ex:alice a-t-il proposé dans cette ontologie ? ».
Interprétation — la provenance comme mémoire du geste
RDF-star transforme un triplet en citoyen de premier ordre : il peut lui-même être sujet d’un autre triplet. Cette auto-référence est exactement ce qui permet de garder la trace du coup ontologique : non seulement quoi a été ajouté, mais qui l’a ajouté, quand, sur quel fondement.
C’est l’élément qui distingue une modification anonyme d’une modification traçable. Sans RDF-star, le diff de triplets est muet sur ses auteurs.
Exercice 1 — Une sous-classe concurrente
Tâche : reprendre l’état initial g_base du contexte et proposer une extension concurrente :
Déclarer une nouvelle sous-classe Audiobook de Book, disjointe de Ebook (un même individu ne peut pas être les deux).
Ajouter une datatype property duration (domain = Audiobook, range = xsd:integer).
Instancier ex:audio1 rdf:type ex:Audiobook avec ex:duration 3600.
Afficher le diff t_added = g_extension - g_base (la liste des triplets effectivement ajoutés, en notation Turtle).
Pièges à éviter : - Owlrl applique OWL-RL : les déclarations owl:disjointWith sont inférées à la fermeture, pas seulement déclarées. Le diff de triplets ajoutés reste celui des triplets explicites — ne pas inclure les triplets inférés. - La propriété duration doit être distincte de format (range différent). Si vous réutilisez format pour Audiobook, le shape SHACL existant (cardinalité sur Ebook) ne protègera pas Audiobook.
# Exercice 1 a completer.## Etape 1 : partir de g_base (cf cellule "Contexte — l'ontologie de depart" plus haut)# Etape 2 : ajouter la sous-classe Audiobook, disjointe de Ebook# Etape 3 : ajouter la datatype property duration (domain=Audiobook, range=xsd:integer)# Etape 4 : instancier ex:audio1 + ex:duration 3600# Etape 5 : afficher le diff explicite t_added = g_extension - g_base## Indice : reprenez le pattern de l'Exemple résolu 1 (cellule code qui suit# "## Exemple résolu 1 — Proposer une extension") et adaptez les URI / properties.print("Exercice 1 a completer")
Exercice 1 a completer
Exercice 2 — Un shape qui protège deux classes
Tâche : construire un shape SHACL qui couvre deux classes à la fois, et démontrer qu’il les protège indépendamment :
Définir un NodeShapeEbookShape avec sh:targetClass ex:Ebook ET une contrainte sh:property sur format (cardinalité >= 1, datatype = xsd:string).
Définir un NodeShapeAudiobookShape avec sh:targetClass ex:Audiobook ET une contrainte sh:property sur duration (cardinalité >= 1, datatype = xsd:integer).
Valider l’extension g_ext (conforme).
Construire une extension rejetée : ex:bad_ebook rdf:type ex:Ebook SANS format. Le rapport SHACL doit nommer uniquementEbookShape (pas AudiobookShape).
Construire une autre extension rejetée : ex:bad_audio rdf:type ex:Audiobook SANS duration. Le rapport doit nommer uniquementAudiobookShape.
Pièges à éviter : - Si vous oublissez sh:targetClass, le shape n’est jamais déclenché — un rapport de violation vide n’est pas un test, c’est un commentaire. - Le datatype mismatch (un Literal non-xsd:string sur format) doit être un finding, pas deux (cardinalité + datatype). Vérifiez la version de pyshacl : certaines versions dupliquent.
# Exercice 2 a completer.## Etape 1 : construire le graphe de shapes avec deux NodeShape (EbookShape, AudiobookShape)# Etape 2 : valider g_ext (doit etre conforme)# Etape 3 : construire g_bad_ebook = g_ext + (ex:bad_ebook rdf:type ex:Ebook) (SANS format)# Etape 4 : valider g_bad_ebook (rapport doit nommer EbookShape uniquement)# Etape 5 : construire g_bad_audio = g_ext + (ex:bad_audio rdf:type ex:Audiobook) (SANS duration)# Etape 6 : valider g_bad_audio (rapport doit nommer AudiobookShape uniquement)## Indice : reprenez l'Exemple résolu 2 (pyshacl.validate) et adaptez la structure# de g_shapes.print("Exercice 2 a completer")
Exercice 2 a completer
Exercice 3 — Inférences sous deux extensions contrastées
Tâche : comparer le delta d’inférences OWL-RL entre deux extensions de l’état initial g_base :
Reprendre g_base (cf Contexte).
Construire g_ext_a : l’extension de l’Exemple résolu 1 (Ebook, format, ebook1) — avec une déclaration Ebook rdfs:subClassOf ex:Book.
Construire g_ext_b : une extension qui déclare Ebook mais sans la subClassOf (juste Ebook rdf:type owl:Class).
Calculer g_ext_a_inferred et g_ext_b_inferred via owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(...).
Énumérer le delta : delta_a = g_ext_a_inferred - g_base_inferred et delta_b = g_ext_b_inferred - g_base_inferred. Afficher le nombre de triplets et les 5 premiers triplets de chaque delta.
Question à laquelle l’exercice répond : la déclaration rdfs:subClassOf est-elle inférentiellement productive dans OWL-RL, ou bien un simple artifice de présentation ? Si len(delta_a) == len(delta_b), la subClassOf est inerte — un signal à noter.
Pièges à éviter : - owlrl.DeductiveClosure(OWLRL_Semantics).expand(g)mute le graphe en place. Si vous voulez comparer à g_base_inferred, faites une copie de g_base avant expand. - rdfs:subClassOf n’est pas transitive par accident : OWL-RL l’applique, mais l’effet dépend de la profondeur du graphe. Si Book n’a pas de parent déclaré, la chaîne s’arrête à Ebook.
# Exercice 3 a completer.## Etape 1 : partir de g_base# Etape 2 : construire g_ext_a (Ebook + subClassOf Book + format + ebook1)# Etape 3 : construire g_ext_b (Ebook SANS subClassOf, mais avec format + ebook1)# Etape 4 : copier puis expand() chaque extension# Etape 5 : enumerer et comparer les deltas## Indice : owlrl.DeductiveClosure(owlrl.OWLRL_Semantics).expand(g) mute en place.# Faites des copies explicites.print("Exercice 3 a completer")
Exercice 3 a completer
Conclusion — les 4 gestes orchestrés
Geste
Outil
Ce qu’il exhibe
Déclarer
OWL (rdflib)
Le diff de triplets ajoutés
Valider
SHACL (pyshacl)
L’admissibilité (conforme / rejeté)
Inférer
OWL-RL (owlrl)
Le delta de conséquences nouvelles
Provenir
RDF-star (rdflib)
L’auteur et la date du coup
Ces 4 gestes ensemble couvrent ce que le dépôt appelle l’extension exécutable, vérifiable et traçable du vocabulaire. Aucun des 4 n’est optionnel : supprimer l’un et l’extension perd sa qualité.
Pourquoi le coup nul est un résultat (pas un échec) : la mesure « nombre de conséquences nouvelles » est falsifiable, donc informative. Si η n’apporte rien, η est à reformuler — pas à célébrer.
Limite : owlrl applique OWL-RL (un fragment décidable), pas OWL-DL complet. Pour des inférences au-delà de OWL-RL (par exemple les types union, l’inégalité), un raisonneur externe (HermiT, Pellet, FaCT++) serait nécessaire. Pour la démonstration de coup nul vs coup productif, OWL-RL suffit largement.