SW-7-OWL

Navigation : << 6-RDFS | Index | 8-SHACL C# >>

OWL 2 : Ontologies et Raisonnement

Objectifs d’apprentissage

A la fin de ce notebook, vous saurez : 1. Comprendre OWL 2 et ce qu’il apporte au-dela de RDFS 2. Utiliser OntologyGraph de dotNetRDF pour manipuler des ontologies 3. Définir des classes OWL avec equivalence, disjonction et restrictions 4. Définir des proprietes OWL : ObjectProperty, DatatypeProperty, FunctionalProperty, inverseOf 5. Distinguer les profils OWL 2 (EL, QL, RL) et leurs cas d’usage 6. Charger et interroger une ontologie existante (university.owl)

Concepts cles

Concept Description
OWL 2 Langage d’ontologie du W3C, extension de RDFS avec logique descriptive
Ontologie Modèle formel d’un domaine : classes, proprietes, contraintes, individus
OntologyGraph Classe dotNetRDF specialisee pour manipuler les ontologies
Logique descriptive Formalisme logique sous-jacent a OWL (decidable)

Prerequis

  • .NET SDK 9.0+ et .NET Interactive
  • Notebooks SW-1 a SW-6 (bases RDF, SPARQL, RDFS)

Duree estimee : 50 minutes


Installation et imports

Chargeons dotNetRDF avec les espaces de noms necessaires pour travailler avec OWL.

#r "nuget: dotNetRDF, 3.2.1"

using System;
using System.Linq;
using System.Collections.Generic;
using VDS.RDF;
using VDS.RDF.Parsing;
using VDS.RDF.Writing;
using VDS.RDF.Ontology;
using VDS.RDF.Query;
using VDS.RDF.Query.Datasets;
using VDS.RDF.Query.Inference;

Console.WriteLine("dotNetRDF charge avec succes.");
Console.WriteLine("Espace de noms OWL disponible : VDS.RDF.Ontology");
Installed Packages
  • dotNetRDF, 3.2.1
dotNetRDF charge avec succes.
Espace de noms OWL disponible : VDS.RDF.Ontology

Interpretation

L’espace de noms VDS.RDF.Ontology est la cle pour travailler avec OWL dans dotNetRDF. Il fournit :

Classe Rôle
OntologyGraph Graphe specialise avec acces aux classes et proprietes OWL
OntologyClass Represente une classe OWL (avec super/sous-classes, instances)
OntologyProperty Represente une propriete OWL (avec domaine, portee)
OntologyResource Classe de base pour les ressources ontologiques

1. Introduction a OWL 2 : au-dela de RDFS

Dans le notebook précédent (SW-6), nous avons vu que RDFS permet de définir des classes, des hiérarchies et des proprietes avec inference. Cependant, RDFS a des limites importantes.

OWL 2 (Web Ontology Language) est base sur la logique descriptive (Description Logic, DL), un fragment decidable de la logique du premier ordre.

OWL 2 (Web Ontology Language) est le standard W3C de modelisation d’ontologies (Motik, Patel-Schneider & Cuenca Grau eds., W3C Recommendation 2009/2012). Il repose formellement sur la logique descriptive (Description Logic, DL) – un fragment decidable de la logique du premier ordre dont la théorie est synthetisee dans Baader, Calvanese, McGuinness, Nardi & Patel-Schneider, The Description Logic Handbook (Cambridge University Press, 2e ed. 2007). Le vocabulaire DL (Concept / Rôle / Individu / TBox / ABox) introduit ci-dessous est celui de cette tradition.

Comparaison RDFS vs OWL 2

Capacite RDFS OWL 2
Hiérarchie de classes rdfs:subClassOf rdfs:subClassOf + restrictions
Classes disjointes Impossible owl:disjointWith
Equivalence de classes Impossible owl:equivalentClass
Cardinalite (min/max) Impossible owl:minCardinality, owl:maxCardinality
Proprietes inverses Impossible owl:inverseOf
Proprietes fonctionnelles Impossible owl:FunctionalProperty
Union/intersection de classes Impossible owl:unionOf, owl:intersectionOf
Raisonnement decidable Limite Garanti (selon le profil)

OWL 2 dans la pile du Web Sémantique

flowchart TD
    SHACL["SHACL (SW-8) - Validation"] --> OWL["OWL 2 (SW-7) - Logique descriptive - NOUS SOMMES ICI"]
    OWL --> RDFS["RDFS (SW-6) - Vocabulaires, hierarchies"]
    RDFS --> SPARQL["SPARQL (SW-5) - Interrogation"]
    SPARQL --> RDF["RDF (SW-1/2/3) - Triplets, graphes"]
    style OWL font-weight:bold

Concepts de la logique descriptive

Terme DL Equivalent OWL Exemple
Concept owl:Class Personne, Cours, Departement
Rôle owl:ObjectProperty enseigne, inscritA
Individu Instance prof_dupont, etud_martin
TBox Axiomes terminologiques “Un Professeur est une Personne”
ABox Assertions individuelles “prof_dupont est un Professeur”

2. OntologyGraph : la classe specialisee de dotNetRDF

OntologyGraph etend Graph avec des méthodes spécifiques pour naviguer dans les ontologies OWL et RDFS. Creons une première ontologie simple en code.

// Creer une ontologie simple avec OntologyGraph
OntologyGraph onto = new OntologyGraph();
onto.NamespaceMap.AddNamespace("uni", new Uri("http://example.org/university#"));
onto.NamespaceMap.AddNamespace("owl", new Uri("http://www.w3.org/2002/07/owl#"));
onto.NamespaceMap.AddNamespace("rdf", new Uri("http://www.w3.org/1999/02/22-rdf-syntax-ns#"));
onto.NamespaceMap.AddNamespace("rdfs", new Uri("http://www.w3.org/2000/01/rdf-schema#"));
onto.NamespaceMap.AddNamespace("xsd", new Uri("http://www.w3.org/2001/XMLSchema#"));

// Noeuds predicats
IUriNode rdfType = onto.CreateUriNode("rdf:type");
IUriNode owlClass = onto.CreateUriNode("owl:Class");
IUriNode subClassOf = onto.CreateUriNode("rdfs:subClassOf");
IUriNode rdfsLabel = onto.CreateUriNode("rdfs:label");

// Definir les classes OWL
IUriNode person = onto.CreateUriNode("uni:Person");
IUriNode student = onto.CreateUriNode("uni:Student");
IUriNode professor = onto.CreateUriNode("uni:Professor");
IUriNode course = onto.CreateUriNode("uni:Course");
IUriNode department = onto.CreateUriNode("uni:Department");

// Declarer comme owl:Class
foreach (var cls in new[] { person, student, professor, course, department })
    onto.Assert(new Triple(cls, rdfType, owlClass));

// Hierarchie
onto.Assert(new Triple(student, subClassOf, person));
onto.Assert(new Triple(professor, subClassOf, person));

// Etiquettes
onto.Assert(new Triple(person, rdfsLabel, onto.CreateLiteralNode("Personne", "fr")));
onto.Assert(new Triple(student, rdfsLabel, onto.CreateLiteralNode("Etudiant", "fr")));
onto.Assert(new Triple(professor, rdfsLabel, onto.CreateLiteralNode("Professeur", "fr")));
onto.Assert(new Triple(course, rdfsLabel, onto.CreateLiteralNode("Cours", "fr")));
onto.Assert(new Triple(department, rdfsLabel, onto.CreateLiteralNode("Departement", "fr")));

Console.WriteLine($"Ontologie creee : {onto.Triples.Count} triplets");

// Utiliser l'API OntologyGraph pour explorer
Console.WriteLine("\nClasses OWL trouvees :");
foreach (OntologyClass cls in onto.OwlClasses)
{
    string uri = cls.Resource.ToString();
    if (uri.Contains("university#"))
    {
        string localName = uri.Split('#').Last();
        Console.WriteLine($"  - {localName}");
    }
}

Console.WriteLine();

// Naviguer dans la hierarchie avec OntologyClass
Console.WriteLine("Hierarchie via OntologyClass :");
foreach (OntologyClass cls in onto.OwlClasses)
{
    string uri = cls.Resource.ToString();
    if (!uri.Contains("university#")) continue;
    string name = uri.Split('#').Last();

    var superClasses = cls.SuperClasses
        .Where(sc => sc.Resource.ToString().Contains("university#"))
        .Select(sc => sc.Resource.ToString().Split('#').Last());
    if (superClasses.Any())
        Console.WriteLine($"  {name} rdfs:subClassOf {string.Join(", ", superClasses)}");

    var subClasses = cls.SubClasses
        .Where(sc => sc.Resource.ToString().Contains("university#"))
        .Select(sc => sc.Resource.ToString().Split('#').Last());
    if (subClasses.Any())
        Console.WriteLine($"  {name} a pour sous-classes : {string.Join(", ", subClasses)}");
}
Ontologie creee : 12 triplets

Classes OWL trouvees :
  - Person
  - Student
  - Professor
  - Course
  - Department

Hierarchie via OntologyClass :
  Person a pour sous-classes : Student, Professor
  Student rdfs:subClassOf Person
  Professor rdfs:subClassOf Person

Interpretation : OntologyGraph vs Graph

Aspect Graph OntologyGraph
Triplets Oui Oui (herite de Graph)
OwlClasses Non Oui – enumere les classes owl:Class
RdfClasses Non Oui – enumere les classes rdfs:Class
AllClasses Non Oui – union des deux
OwlProperties Non Oui – proprietes OWL
Navigation sémantique Non SuperClasses, SubClasses, Instances

Points cles : 1. onto.OwlClasses retourne uniquement les ressources typees owl:Class 2. OntologyClass.SuperClasses et SubClasses retournent les relations directes seulement 3. Pour obtenir la fermeture transitive, il faut iterer ou utiliser un raisonneur


3. Classes OWL : equivalence, disjonction, restrictions

OWL 2 offre des constructeurs de classes bien plus riches que RDFS :

Constructeur Syntaxe OWL Description
Classe nommee owl:Class Classe définie par un URI
Equivalence owl:equivalentClass Deux classes ont exactement les mêmes instances
Disjonction owl:disjointWith Aucune instance commune
Complement owl:complementOf Tout ce qui n’est PAS dans la classe
Union owl:unionOf Instances de C1 OU C2
Intersection owl:intersectionOf Instances de C1 ET C2
Restriction owl:Restriction Classe définie par une contrainte sur une propriete

Demontrons owl:equivalentClass et owl:disjointWith en code.

// Demonstration de owl:equivalentClass et owl:disjointWith
OntologyGraph classDemo = new OntologyGraph();
classDemo.NamespaceMap.AddNamespace("ex", new Uri("http://example.org/transport#"));
classDemo.NamespaceMap.AddNamespace("owl", new Uri("http://www.w3.org/2002/07/owl#"));
classDemo.NamespaceMap.AddNamespace("rdf", new Uri("http://www.w3.org/1999/02/22-rdf-syntax-ns#"));
classDemo.NamespaceMap.AddNamespace("rdfs", new Uri("http://www.w3.org/2000/01/rdf-schema#"));

IUriNode rdfTypeC = classDemo.CreateUriNode("rdf:type");
IUriNode owlClassC = classDemo.CreateUriNode("owl:Class");
IUriNode equivalentClass = classDemo.CreateUriNode("owl:equivalentClass");
IUriNode disjointWith = classDemo.CreateUriNode("owl:disjointWith");
IUriNode subClassOfC = classDemo.CreateUriNode("rdfs:subClassOf");

// Classes
IUriNode vehicule = classDemo.CreateUriNode("ex:Vehicule");
IUriNode voiture = classDemo.CreateUriNode("ex:Voiture");
IUriNode automobile = classDemo.CreateUriNode("ex:Automobile");
IUriNode moto = classDemo.CreateUriNode("ex:Moto");
IUriNode velo = classDemo.CreateUriNode("ex:Velo");

// Declarer les classes
foreach (var cls in new[] { vehicule, voiture, automobile, moto, velo })
    classDemo.Assert(new Triple(cls, rdfTypeC, owlClassC));

// Hierarchie
classDemo.Assert(new Triple(voiture, subClassOfC, vehicule));
classDemo.Assert(new Triple(moto, subClassOfC, vehicule));
classDemo.Assert(new Triple(velo, subClassOfC, vehicule));

// owl:equivalentClass : Voiture = Automobile (ce sont des synonymes)
classDemo.Assert(new Triple(voiture, equivalentClass, automobile));

// owl:disjointWith : une Voiture ne peut pas etre une Moto, etc.
classDemo.Assert(new Triple(voiture, disjointWith, moto));
classDemo.Assert(new Triple(voiture, disjointWith, velo));
classDemo.Assert(new Triple(moto, disjointWith, velo));

Console.WriteLine($"Ontologie vehicules : {classDemo.Triples.Count} triplets");
Console.WriteLine();

// Afficher les equivalences
Console.WriteLine("Equivalences (owl:equivalentClass) :");
foreach (Triple t in classDemo.GetTriplesWithPredicate(equivalentClass))
{
    string s = ((IUriNode)t.Subject).Uri.Fragment.TrimStart('#');
    string o = ((IUriNode)t.Object).Uri.Fragment.TrimStart('#');
    Console.WriteLine($"  {s} = {o}");
}

Console.WriteLine();

// Afficher les disjonctions
Console.WriteLine("Disjonctions (owl:disjointWith) :");
foreach (Triple t in classDemo.GetTriplesWithPredicate(disjointWith))
{
    string s = ((IUriNode)t.Subject).Uri.Fragment.TrimStart('#');
    string o = ((IUriNode)t.Object).Uri.Fragment.TrimStart('#');
    Console.WriteLine($"  {s} disjoint de {o}");
}
Ontologie vehicules : 12 triplets

Equivalences (owl:equivalentClass) :
  Voiture = Automobile

Disjonctions (owl:disjointWith) :
  Voiture disjoint de Moto
  Voiture disjoint de Velo
  Moto disjoint de Velo

Interpretation : Equivalence et disjonction

Relation Signification Consequence logique
Voiture owl:equivalentClass Automobile Synonymes parfaits Toute Voiture est une Automobile et vice-versa
Voiture owl:disjointWith Moto Ensembles disjoints Aucun individu ne peut etre a la fois Voiture et Moto
Moto owl:disjointWith Velo Ensembles disjoints Aucun individu ne peut etre a la fois Moto et Velo

Différence entre equivalentClass et subClassOf : - A rdfs:subClassOf B : toute instance de A est instance de B (inclusion, \(A \subseteq B\)) - A owl:equivalentClass B : A et B ont exactement les mêmes instances (\(A = B\))

Cas d’usage : owl:equivalentClass est souvent utilise pour relier des termes de vocabulaires différents (ex: schema:Person owl:equivalentClass foaf:Person).


4. Proprietes OWL

OWL distingue deux types fondamentaux de proprietes et offre des caractéristiques supplementaires :

Types de proprietes

Type Declaration Description
Object Property owl:ObjectProperty Relie deux individus (sujet et objet sont des ressources)
Datatype Property owl:DatatypeProperty Relie un individu a une valeur litterale

Caractéristiques des proprietes

Caractéristique Syntaxe Signification
Fonctionnelle owl:FunctionalProperty Au plus une valeur par sujet
Inverse-fonctionnelle owl:InverseFunctionalProperty Au plus un sujet par valeur
Transitive owl:TransitiveProperty Si a->b et b->c alors a->c
Symetrique owl:SymmetricProperty Si a->b alors b->a
Inverse owl:inverseOf P1 est l’inverse de P2

Construisons un exemple complet avec les proprietes d’une ontologie universitaire.

// Proprietes OWL : types et caracteristiques
OntologyGraph propDemo = new OntologyGraph();
propDemo.NamespaceMap.AddNamespace("uni", new Uri("http://example.org/university#"));
propDemo.NamespaceMap.AddNamespace("owl", new Uri("http://www.w3.org/2002/07/owl#"));
propDemo.NamespaceMap.AddNamespace("rdf", new Uri("http://www.w3.org/1999/02/22-rdf-syntax-ns#"));
propDemo.NamespaceMap.AddNamespace("rdfs", new Uri("http://www.w3.org/2000/01/rdf-schema#"));
propDemo.NamespaceMap.AddNamespace("xsd", new Uri("http://www.w3.org/2001/XMLSchema#"));

IUriNode rdfTypeP = propDemo.CreateUriNode("rdf:type");
IUriNode owlObjProp = propDemo.CreateUriNode("owl:ObjectProperty");
IUriNode owlDatatypeProp = propDemo.CreateUriNode("owl:DatatypeProperty");
IUriNode owlFunctional = propDemo.CreateUriNode("owl:FunctionalProperty");
IUriNode owlInverseOf = propDemo.CreateUriNode("owl:inverseOf");
IUriNode rdfsDomainP = propDemo.CreateUriNode("rdfs:domain");
IUriNode rdfsRangeP = propDemo.CreateUriNode("rdfs:range");
IUriNode rdfsLabelP = propDemo.CreateUriNode("rdfs:label");
IUriNode owlClassP = propDemo.CreateUriNode("owl:Class");

// Classes (necessaires pour domaine/portee)
IUriNode personP = propDemo.CreateUriNode("uni:Person");
IUriNode studentP = propDemo.CreateUriNode("uni:Student");
IUriNode professorP = propDemo.CreateUriNode("uni:Professor");
IUriNode courseP = propDemo.CreateUriNode("uni:Course");
IUriNode deptP = propDemo.CreateUriNode("uni:Department");

foreach (var c in new[] { personP, studentP, professorP, courseP, deptP })
    propDemo.Assert(new Triple(c, rdfTypeP, owlClassP));

// --- Object Properties ---
// uni:teaches (Professor -> Course)
IUriNode teaches = propDemo.CreateUriNode("uni:teaches");
propDemo.Assert(new Triple(teaches, rdfTypeP, owlObjProp));
propDemo.Assert(new Triple(teaches, rdfsDomainP, professorP));
propDemo.Assert(new Triple(teaches, rdfsRangeP, courseP));
propDemo.Assert(new Triple(teaches, rdfsLabelP, propDemo.CreateLiteralNode("enseigne", "fr")));

// uni:taughtBy (Course -> Professor) - INVERSE de teaches
IUriNode taughtBy = propDemo.CreateUriNode("uni:taughtBy");
propDemo.Assert(new Triple(taughtBy, rdfTypeP, owlObjProp));
propDemo.Assert(new Triple(taughtBy, rdfsDomainP, courseP));
propDemo.Assert(new Triple(taughtBy, rdfsRangeP, professorP));
propDemo.Assert(new Triple(taughtBy, owlInverseOf, teaches));

// uni:enrolledIn (Student -> Course)
IUriNode enrolledIn = propDemo.CreateUriNode("uni:enrolledIn");
propDemo.Assert(new Triple(enrolledIn, rdfTypeP, owlObjProp));
propDemo.Assert(new Triple(enrolledIn, rdfsDomainP, studentP));
propDemo.Assert(new Triple(enrolledIn, rdfsRangeP, courseP));

// --- Datatype Properties ---
// uni:name (Person -> xsd:string) - FONCTIONNELLE (au plus 1 nom)
IUriNode nameProp = propDemo.CreateUriNode("uni:name");
propDemo.Assert(new Triple(nameProp, rdfTypeP, owlDatatypeProp));
propDemo.Assert(new Triple(nameProp, rdfTypeP, owlFunctional)); // Au plus 1 valeur
propDemo.Assert(new Triple(nameProp, rdfsDomainP, personP));
propDemo.Assert(new Triple(nameProp, rdfsRangeP, propDemo.CreateUriNode("xsd:string")));

// uni:credits (Course -> xsd:integer)
IUriNode credits = propDemo.CreateUriNode("uni:credits");
propDemo.Assert(new Triple(credits, rdfTypeP, owlDatatypeProp));
propDemo.Assert(new Triple(credits, rdfsDomainP, courseP));
propDemo.Assert(new Triple(credits, rdfsRangeP, propDemo.CreateUriNode("xsd:integer")));

Console.WriteLine($"Ontologie avec proprietes : {propDemo.Triples.Count} triplets");
Console.WriteLine();

// Afficher les Object Properties
Console.WriteLine("Object Properties :");
foreach (Triple t in propDemo.GetTriplesWithPredicateObject(rdfTypeP, owlObjProp))
{
    string name = ((IUriNode)t.Subject).Uri.Fragment.TrimStart('#');
    var dom = propDemo.GetTriplesWithSubjectPredicate(t.Subject, rdfsDomainP);
    var rng = propDemo.GetTriplesWithSubjectPredicate(t.Subject, rdfsRangeP);
    string d = dom.Any() ? ((IUriNode)dom.First().Object).Uri.Fragment.TrimStart('#') : "?";
    string r = rng.Any() ? ((IUriNode)rng.First().Object).Uri.Fragment.TrimStart('#') : "?";
    Console.WriteLine($"  {name} : {d} -> {r}");
}

Console.WriteLine();

// Afficher les Datatype Properties
Console.WriteLine("Datatype Properties :");
foreach (Triple t in propDemo.GetTriplesWithPredicateObject(rdfTypeP, owlDatatypeProp))
{
    string name = ((IUriNode)t.Subject).Uri.Fragment.TrimStart('#');
    var dom = propDemo.GetTriplesWithSubjectPredicate(t.Subject, rdfsDomainP);
    var rng = propDemo.GetTriplesWithSubjectPredicate(t.Subject, rdfsRangeP);
    string d = dom.Any() ? ((IUriNode)dom.First().Object).Uri.Fragment.TrimStart('#') : "?";
    string r = rng.Any() ? ((IUriNode)rng.First().Object).Uri.Fragment.TrimStart('#') : "?";
    bool isFunctional = propDemo.GetTriplesWithSubjectPredicate(t.Subject, rdfTypeP)
        .Any(tr => tr.Object.ToString().Contains("FunctionalProperty"));
    string funcTag = isFunctional ? " [fonctionnelle]" : "";
    Console.WriteLine($"  {name} : {d} -> {r}{funcTag}");
}

Console.WriteLine();

// Afficher les proprietes inverses
Console.WriteLine("Proprietes inverses (owl:inverseOf) :");
foreach (Triple t in propDemo.GetTriplesWithPredicate(owlInverseOf))
{
    string s = ((IUriNode)t.Subject).Uri.Fragment.TrimStart('#');
    string o = ((IUriNode)t.Object).Uri.Fragment.TrimStart('#');
    Console.WriteLine($"  {s} est l'inverse de {o}");
}
Ontologie avec proprietes : 23 triplets

Object Properties :
  teaches : Professor -> Course
  taughtBy : Course -> Professor
  enrolledIn : Student -> Course

Datatype Properties :
  name : Person -> string [fonctionnelle]
  credits : Course -> integer

Proprietes inverses (owl:inverseOf) :
  taughtBy est l'inverse de teaches

Interpretation : Proprietes OWL

Propriete Type Domaine Portee Particularite
teaches ObjectProperty Professor Course –
taughtBy ObjectProperty Course Professor Inverse de teaches
enrolledIn ObjectProperty Student Course –
name DatatypeProperty Person xsd:string Fonctionnelle (au plus 1 valeur)
credits DatatypeProperty Course xsd:integer –

Points cles : 1. ObjectProperty relie deux individus : prof_dupont teaches course_ia 2. DatatypeProperty relie un individu a une valeur : prof_dupont name "Marie Dupont" 3. FunctionalProperty garantit l’unicite : une personne a au plus un nom 4. owl:inverseOf permet la navigation bidirectionnelle : si “Dupont enseigne IA”, alors “IA est enseigne par Dupont”

En RDFS, rdf:Property ne distingue pas ObjectProperty et DatatypeProperty. OWL rend cette distinction explicite, ce qui ameliore la validation et le raisonnement.


5. Profils OWL 2 : EL, QL, RL

OWL 2 définit trois profils (sous-ensembles) qui offrent des compromis entre expressivite et performances de raisonnement :

Profil Logique Expressivite Performance Cas d’usage
OWL 2 EL \(\mathcal{EL}^{++}\) Limitee Temps polynomial Grandes ontologies biomedicales (SNOMED CT, Gene Ontology)
OWL 2 QL DL-Lite Limitee Reductible a SQL Acces par requêtes SPARQL/SQL, OBDA
OWL 2 RL Règles Moderee Implementable par règles Systèmes a base de règles, validation
OWL 2 DL \(\mathcal{SROIQ}(D)\) Maximale Decidable mais couteux Ontologies complexes, raisonnement complet

Les logiques sous-jacentes aux profils sont des fragments DL nommes et etudies : \(\mathcal{EL}^{++}\) pour OWL 2 EL (Baader, Brandt & Lutz, “Pushing the EL Envelope”, IJCAI 2005), DL-Lite pour OWL 2 QL (Calvanese, De Giacomo, Lembo, Lenzerini & Rosati, 2007) et \(\mathcal{SROIQ}(D)\) pour OWL 2 DL (Horrocks, Kutz & Sattler, 2006). Les profils eux-mêmes sont specifies par le W3C (Motik et al., OWL 2 Profiles, 2009).

Comparaison detaillee

Critere EL QL RL
Conjonction de classes Oui Non Oui
Quantification existentielle Oui Limitee Non
Negation Non Oui (limitee) Oui (limitee)
Cardinalite Non Non Oui (limitee)
Nombre de classes > 100 000 > 10 000 > 10 000
Implementation typique Classificateur Rewriting SPARQL Règles de production

Comment choisir ?

Votre ontologie est-elle très grande (>100K classes) ?
  --> OUI : OWL 2 EL
  --> NON :
      Acces par requêtes SQL/SPARQL principalement ?
        --> OUI : OWL 2 QL
        --> NON :
            Implementation par règles ?
              --> OUI : OWL 2 RL
              --> NON : OWL 2 DL (expressivite maximale)

Exemples d’ontologies reelles

Ontologie Profil Domaine Taille
SNOMED CT EL Terminologie medicale ~350 000 concepts
Gene Ontology EL Bioinformatique ~45 000 termes
Schema.org RL SEO, données structurees ~800 types
DBpedia Ontology RL Connaissances générales ~760 classes
CIDOC-CRM DL Patrimoine culturel ~90 classes

6. Exemple guide avec data/university.owl

Le fichier data/university.owl contient une petite ontologie OWL 2 au format RDF/XML. Chargeons-la avec OntologyGraph et explorons sa structure.

// Charger l'ontologie university.owl
OntologyGraph uniOnto = new OntologyGraph();
RdfXmlParser rdfXmlParser = new RdfXmlParser();
rdfXmlParser.Load(uniOnto, "data/university.owl");

Console.WriteLine($"Ontologie chargee : {uniOnto.Triples.Count} triplets");
Console.WriteLine($"Namespaces : {string.Join(", ", uniOnto.NamespaceMap.Prefixes)}");
Console.WriteLine();

// Lister les classes OWL
Console.WriteLine("=== Classes OWL ===");
foreach (OntologyClass cls in uniOnto.OwlClasses)
{
    string uri = cls.Resource.ToString();
    if (!uri.Contains("university#")) continue;
    string name = uri.Split('#').Last();

    // Chercher le label
    IUriNode labelNode = uniOnto.CreateUriNode(new Uri("http://www.w3.org/2000/01/rdf-schema#label"));
    var labels = uniOnto.GetTriplesWithSubjectPredicate(cls.Resource, labelNode);
    string label = labels.Any() ? ((ILiteralNode)labels.First().Object).Value : "";

    Console.Write($"  {name}");
    if (!string.IsNullOrEmpty(label))
        Console.Write($" ({label})");
    Console.WriteLine();

    // Super-classes
    var superClasses = cls.SuperClasses
        .Where(sc => sc.Resource.ToString().Contains("university#"))
        .Select(sc => sc.Resource.ToString().Split('#').Last());
    if (superClasses.Any())
        Console.WriteLine($"    rdfs:subClassOf {string.Join(", ", superClasses)}");
}
Ontologie chargee : 64 triplets
Namespaces : rdf, rdfs, xsd, owl, xml, uni

=== Classes OWL ===
  Person (Personne)
  Student (Etudiant)
    rdfs:subClassOf Person
  Professor (Professeur)
    rdfs:subClassOf Person
  Course (Cours)
  Department (Departement)
  GraduateStudent (Etudiant en master/doctorat)
    rdfs:subClassOf Student, Person

Interpretation : Structure de university.owl

L’ontologie contient :

Élément Exemples
Classes Person, Student, Professor, GraduateStudent, Course, Department
Hiérarchie Student < Person, Professor < Person, GraduateStudent < Student
Disjonction Professor disjointWith Student
ObjectProperties enrolledIn, teaches, memberOf, advisedBy
DatatypeProperties name (fonctionnelle), credits
Instances prof_dupont, etud_martin, etud_bernard, course_ia, course_web_sem

Interroger les proprietes et individus

Explorons les proprietes de l’ontologie et les relations entre individus.

// Explorer les proprietes et individus de university.owl
IUriNode rdfTypeU = uniOnto.CreateUriNode(new Uri("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"));
IUriNode owlObjPropU = uniOnto.CreateUriNode(new Uri("http://www.w3.org/2002/07/owl#ObjectProperty"));
IUriNode owlDtPropU = uniOnto.CreateUriNode(new Uri("http://www.w3.org/2002/07/owl#DatatypeProperty"));
IUriNode rdfsDomainU = uniOnto.CreateUriNode(new Uri("http://www.w3.org/2000/01/rdf-schema#domain"));
IUriNode rdfsRangeU = uniOnto.CreateUriNode(new Uri("http://www.w3.org/2000/01/rdf-schema#range"));
IUriNode rdfsLabelU = uniOnto.CreateUriNode(new Uri("http://www.w3.org/2000/01/rdf-schema#label"));

Console.WriteLine("=== Object Properties ===");
foreach (Triple t in uniOnto.GetTriplesWithPredicateObject(rdfTypeU, owlObjPropU))
{
    string name = ((IUriNode)t.Subject).Uri.Fragment.TrimStart('#');
    var dom = uniOnto.GetTriplesWithSubjectPredicate(t.Subject, rdfsDomainU);
    var rng = uniOnto.GetTriplesWithSubjectPredicate(t.Subject, rdfsRangeU);
    var lbl = uniOnto.GetTriplesWithSubjectPredicate(t.Subject, rdfsLabelU);
    string d = dom.Any() ? ((IUriNode)dom.First().Object).Uri.Fragment.TrimStart('#') : "?";
    string r = rng.Any() ? ((IUriNode)rng.First().Object).Uri.Fragment.TrimStart('#') : "?";
    string l = lbl.Any() ? ((ILiteralNode)lbl.First().Object).Value : "";
    Console.WriteLine($"  {name} ({l}) : {d} -> {r}");
}

Console.WriteLine();
Console.WriteLine("=== Datatype Properties ===");
foreach (Triple t in uniOnto.GetTriplesWithPredicateObject(rdfTypeU, owlDtPropU))
{
    string name = ((IUriNode)t.Subject).Uri.Fragment.TrimStart('#');
    var dom = uniOnto.GetTriplesWithSubjectPredicate(t.Subject, rdfsDomainU);
    var rng = uniOnto.GetTriplesWithSubjectPredicate(t.Subject, rdfsRangeU);
    string d = dom.Any() ? ((IUriNode)dom.First().Object).Uri.Fragment.TrimStart('#') : "?";
    string r = rng.Any() ? ((IUriNode)rng.First().Object).Uri.Fragment.TrimStart('#') : "?";
    Console.WriteLine($"  {name} : {d} -> {r}");
}
=== Object Properties ===
  enrolledIn (inscrit a) : Student -> Course
  teaches (enseigne) : Professor -> Course
  memberOf (membre de) : Person -> Department
  advisedBy (dirige par) : GraduateStudent -> Professor

=== Datatype Properties ===
  name : Person -> string
  credits : Course -> integer

Interpretation : Proprietes de university.owl

Propriete Type Domaine Portee Description
enrolledIn ObjectProperty Student Course Inscription a un cours
teaches ObjectProperty Professor Course Enseignement
memberOf ObjectProperty Person Department Appartenance
advisedBy ObjectProperty GraduateStudent Professor Direction de these
name DatatypeProperty Person xsd:string Nom de la personne
credits DatatypeProperty Course xsd:integer Nombre de credits ECTS

Requêtes SPARQL sur l’ontologie

Utilisons SPARQL pour interroger les individus et croiser les informations.

// Requetes SPARQL sur university.owl
var uniDataset = new InMemoryDataset(uniOnto);
var uniProcessor = new LeviathanQueryProcessor(uniDataset);
var sparqlParser = new SparqlQueryParser();

// Requete 1 : Lister toutes les personnes avec leur type
string q1 = @"
PREFIX uni: <http://example.org/university#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

SELECT ?person ?name ?type
WHERE {
    ?person rdf:type ?type .
    ?type rdfs:subClassOf* uni:Person .
    ?person uni:name ?name .
}
ORDER BY ?name
";

SparqlResultSet r1 = (SparqlResultSet)uniProcessor.ProcessQuery(sparqlParser.ParseFromString(q1));

Console.WriteLine("Personnes dans l'ontologie :");
Console.WriteLine("---------------------------");
foreach (SparqlResult r in r1)
{
    string name = ((ILiteralNode)r["name"]).Value;
    string type = ((IUriNode)r["type"]).Uri.Fragment.TrimStart('#');
    Console.WriteLine($"  {name} (type: {type})");
}

Console.WriteLine();

// Requete 2 : Qui enseigne quoi ?
string q2 = @"
PREFIX uni: <http://example.org/university#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

SELECT ?profName ?courseName ?credits
WHERE {
    ?prof uni:teaches ?course .
    ?prof uni:name ?profName .
    ?course rdfs:label ?courseName .
    OPTIONAL { ?course uni:credits ?credits . }
}
ORDER BY ?profName ?courseName
";

SparqlResultSet r2 = (SparqlResultSet)uniProcessor.ProcessQuery(sparqlParser.ParseFromString(q2));

Console.WriteLine("Enseignements :");
Console.WriteLine("---------------");
foreach (SparqlResult r in r2)
{
    string prof = ((ILiteralNode)r["profName"]).Value;
    string course = ((ILiteralNode)r["courseName"]).Value;
    string cred = r.HasBoundValue("credits") ? ((ILiteralNode)r["credits"]).Value : "?";
    Console.WriteLine($"  {prof} enseigne '{course}' ({cred} credits)");
}
Personnes dans l'ontologie :
---------------------------
  Marie Dupont (type: Professor)
  Pierre Martin (type: GraduateStudent)
  Sophie Bernard (type: Student)

Enseignements :
---------------
  Marie Dupont enseigne 'Intelligence Artificielle' (6 credits)
  Marie Dupont enseigne 'Web Semantique' (4 credits)

Interpretation : Requêtes sur l’ontologie

Personne Type Cours Rôle
Marie Dupont Professor IA, Web Sémantique Enseigne
Pierre Martin GraduateStudent IA, Web Sémantique Inscrit, dirige par Dupont
Sophie Bernard Student Web Sémantique Inscrite

Points cles : 1. La requête avec rdfs:subClassOf* (chemin de propriete) remonte la hiérarchie de classes 2. OPTIONAL evite de perdre les résultats sans credits 3. L’ontologie permet de croiser les informations de différentes proprietes

Verifier les contraintes OWL

L’ontologie declare Professor owl:disjointWith Student. Verifions cette contrainte et observons ce qui se passe en cas de violation.

// Verifier la disjonction Professor / Student
IUriNode owlDisjointU = uniOnto.CreateUriNode(new Uri("http://www.w3.org/2002/07/owl#disjointWith"));

Console.WriteLine("Contraintes de disjonction :");
foreach (Triple t in uniOnto.GetTriplesWithPredicate(owlDisjointU))
{
    string s = ((IUriNode)t.Subject).Uri.Fragment.TrimStart('#');
    string o = ((IUriNode)t.Object).Uri.Fragment.TrimStart('#');
    Console.WriteLine($"  {s} owl:disjointWith {o}");
}

Console.WriteLine();

// Requete SPARQL pour detecter les violations
string qViolation = @"
PREFIX uni: <http://example.org/university#>
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX owl: <http://www.w3.org/2002/07/owl#>

SELECT ?class1 ?class2 ?individual
WHERE {
    ?class1 owl:disjointWith ?class2 .
    ?individual rdf:type ?class1 .
    ?individual rdf:type ?class2 .
}
";

SparqlResultSet violations = (SparqlResultSet)uniProcessor.ProcessQuery(
    sparqlParser.ParseFromString(qViolation));

if (violations.Count == 0)
{
    Console.WriteLine("Aucune violation detectee. L'ontologie est coherente.");
}
else
{
    Console.WriteLine($"ATTENTION : {violations.Count} violation(s) !");
}

Console.WriteLine();

// Demonstration : ajouter une violation intentionnelle
Console.WriteLine("--- Ajout intentionnel d'une violation ---");
IUriNode profDupont = uniOnto.CreateUriNode(new Uri("http://example.org/university#prof_dupont"));
IUriNode studentType = uniOnto.CreateUriNode(new Uri("http://example.org/university#Student"));
IUriNode rdfTypeV = uniOnto.CreateUriNode(new Uri("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"));

Triple violationTriple = new Triple(profDupont, rdfTypeV, studentType);
uniOnto.Assert(violationTriple);

SparqlResultSet violations2 = (SparqlResultSet)uniProcessor.ProcessQuery(
    sparqlParser.ParseFromString(qViolation));

Console.WriteLine($"Violations detectees : {violations2.Count}");
foreach (SparqlResult r in violations2)
{
    string ind = ((IUriNode)r["individual"]).Uri.Fragment.TrimStart('#');
    string c1 = ((IUriNode)r["class1"]).Uri.Fragment.TrimStart('#');
    string c2 = ((IUriNode)r["class2"]).Uri.Fragment.TrimStart('#');
    Console.WriteLine($"  {ind} est a la fois {c1} et {c2}");
}

// Retirer la violation
uniOnto.Retract(violationTriple);
Console.WriteLine("\nViolation retiree. Ontologie restauree.")
Contraintes de disjonction :
  Professor owl:disjointWith Student

Aucune violation detectee. L'ontologie est coherente.

--- Ajout intentionnel d'une violation ---
Violations detectees : 1
  prof_dupont est a la fois Professor et Student

Violation retiree. Ontologie restauree.

Interpretation : Verification des contraintes

Étape Résultat Explication
Avant violation 0 violation L’ontologie est coherente
Après prof_dupont rdf:type Student 1 violation prof_dupont est Professor ET Student
Après retrait 0 violation Coherence restauree

Points cles : 1. OWL définit des contraintes sémantiques verifiables par SPARQL 2. dotNetRDF ne bloque pas l’ajout de triplets violant une contrainte (hypothese du monde ouvert) 3. C’est au raisonneur ou a la requête de detecter les incoherences 4. Un raisonneur OWL complet (HermiT, Pellet) deduirait que l’ontologie est inconsistante

Hypothese du monde ouvert : en OWL, l’absence d’information ne signifie pas la negation. Le raisonneur ne rejette pas les triplets, il detecte les contradictions.

Appliquer l’inference RDFS a l’ontologie OWL

Bien que dotNetRDF ne fournisse pas de raisonneur OWL DL complet, le StaticRdfsReasoner permet de propager les types dans la hiérarchie.

// Recharger l'ontologie propre pour la demonstration d'inference
Graph uniGraph = new Graph();
RdfXmlParser rdfXmlParser2 = new RdfXmlParser();
rdfXmlParser2.Load(uniGraph, "data/university.owl");

int before = uniGraph.Triples.Count;

// Types de etud_martin avant inference
IUriNode martin = uniGraph.CreateUriNode(new Uri("http://example.org/university#etud_martin"));
IUriNode rdfTypeN = uniGraph.CreateUriNode(new Uri("http://www.w3.org/1999/02/22-rdf-syntax-ns#type"));

Console.WriteLine("Types de etud_martin AVANT inference :");
foreach (Triple t in uniGraph.GetTriplesWithSubjectPredicate(martin, rdfTypeN))
{
    if (t.Object is IUriNode typeUri)
        Console.WriteLine($"  - {typeUri.Uri.Fragment.TrimStart('#')}");
}

// Appliquer le raisonneur RDFS
StaticRdfsReasoner rdfsReasoner = new StaticRdfsReasoner();
rdfsReasoner.Initialise(uniGraph);
rdfsReasoner.Apply(uniGraph);

int after = uniGraph.Triples.Count;
Console.WriteLine($"\nInference : {before} -> {after} triplets (+{after - before} inferes)");

Console.WriteLine("\nTypes de etud_martin APRES inference :");
foreach (Triple t in uniGraph.GetTriplesWithSubjectPredicate(martin, rdfTypeN))
{
    if (t.Object is IUriNode typeUri)
        Console.WriteLine($"  - {typeUri.Uri.Fragment.TrimStart('#')}");
}

Console.WriteLine();
IUriNode dupont = uniGraph.CreateUriNode(new Uri("http://example.org/university#prof_dupont"));
Console.WriteLine("Types de prof_dupont APRES inference :");
foreach (Triple t in uniGraph.GetTriplesWithSubjectPredicate(dupont, rdfTypeN))
{
    if (t.Object is IUriNode typeUri)
        Console.WriteLine($"  - {typeUri.Uri.Fragment.TrimStart('#')}");
}
Types de etud_martin AVANT inference :
  - GraduateStudent

Inference : 64 -> 71 triplets (+7 inferes)

Types de etud_martin APRES inference :
  - GraduateStudent
  - Student
  - Person

Types de prof_dupont APRES inference :
  - Professor
  - Person

Interpretation : Inference sur l’ontologie OWL

Individu Types explicites Types inferes par RDFS
etud_martin GraduateStudent Student, Person
prof_dupont Professor Person
etud_bernard Student Person

Chaîne d’inference pour etud_martin :

etud_martin rdf:type GraduateStudent
GraduateStudent rdfs:subClassOf Student    --> etud_martin rdf:type Student
Student rdfs:subClassOf Person              --> etud_martin rdf:type Person

Limites du raisonneur dotNetRDF : - StaticRdfsReasoner applique uniquement les règles RDFS, pas les règles OWL - Il ne verifie pas owl:disjointWith, owl:FunctionalProperty, etc. - Pour un raisonnement OWL complet, il faut un raisonneur externe (HermiT, Pellet, FaCT++)


Exercices

Exercice 1 : Completer l’ontologie universitaire

Etendez l’ontologie university.owl en ajoutant : - Une classe ResearchProject (owl:Class) - Une propriete worksOn (owl:ObjectProperty, domaine: Professor, portee: ResearchProject) - Une propriete participatesIn (owl:ObjectProperty, domaine: GraduateStudent, portee: ResearchProject) - Deux instances de ResearchProject avec des labels - Rattachez prof_dupont et etud_martin a un projet - Ecrivez une requête SPARQL pour lister tous les participants a chaque projet

Exercice : Explorer les equivalences de classes OWL

La section sur owl:equivalentClass et owl:disjointWith a montre comment modeliser des relations entre classes. Verifiez quelles classes sont equivalentes dans university.owl.

Indices :

  • SPARQL : ?cls1 owl:equivalentClass ?cls2 pour trouver les equivalences
  • g.CreateUriNode("owl:equivalentClass") si necessaire
// Exercice : Explorer les equivalences de classes dans university.owl
// TODO etudiant : chargez university.owl et trouvez toutes les classes equivalentes
//
// Etape 1 : charger le fichier university.owl dans un graphe
// Etape 2 : ecrire une requete SPARQL pour trouver les paires owl:equivalentClass
// Etape 3 : verifier aussi les owl:disjointWith
// Etape 4 : afficher les resultats et interpreter les relations trouvees

Console.WriteLine("Exercice a completer");
Exercice a completer
// Exercice 1 : Completer l'ontologie universitaire

// Recharger l'ontologie propre
OntologyGraph exOnto = new OntologyGraph();
RdfXmlParser exParser = new RdfXmlParser();
exParser.Load(exOnto, "data/university.owl");

// TODO: Ajouter la classe ResearchProject (owl:Class)
// TODO: Ajouter les proprietes worksOn et participatesIn (owl:ObjectProperty)
// TODO: Creer 2 instances de ResearchProject avec rdfs:label
// TODO: Relier prof_dupont et etud_martin aux projets
// TODO: Requete SPARQL pour lister les participants par projet

Console.WriteLine("Exercice 1 : a completer");
Exercice 1 : a completer

Exercice 2 : Detecter les incoherences

Ecrivez un programme qui : 1. Charge university.owl 2. Ajoute volontairement des triplets problematiques : - etud_bernard rdf:type Professor (violation de disjonction) - Un nouveau cours sans credits 3. Ecrivez des requêtes SPARQL pour detecter : - Les violations de disjonction (individu avec deux types disjoints) - Les cours sans credits (propriete manquante) - Les etudiants sans inscription (pas de enrolledIn) 4. Affichez un rapport de coherence

// Exercice 2 : Detecter les incoherences

OntologyGraph checkOnto = new OntologyGraph();
RdfXmlParser checkParser = new RdfXmlParser();
checkParser.Load(checkOnto, "data/university.owl");

// TODO: Ajouter etud_bernard rdf:type Professor (violation)
// TODO: Ajouter un cours sans credits
// TODO: Requete pour violations de disjonction
// TODO: Requete pour cours sans credits
// TODO: Requete pour etudiants sans inscription
// TODO: Afficher un rapport de coherence

Console.WriteLine("Exercice 2 : a completer");
Exercice 2 : a completer

Resume

OWL 2 : vocabulaire essentiel

Terme OWL Utilisation Exemple
owl:Class Declarer une classe uni:Person rdf:type owl:Class
owl:ObjectProperty Propriete entre individus uni:teaches rdf:type owl:ObjectProperty
owl:DatatypeProperty Propriete individu-valeur uni:name rdf:type owl:DatatypeProperty
owl:equivalentClass Synonymie de classes Voiture owl:equivalentClass Automobile
owl:disjointWith Classes exclusives Professor owl:disjointWith Student
owl:FunctionalProperty Au plus une valeur uni:name (un seul nom par personne)
owl:inverseOf Propriete miroir taughtBy owl:inverseOf teaches

Ce que nous avons appris

  1. OWL 2 depasse RDFS avec des classes disjointes, des proprietes typees et des restrictions
  2. OntologyGraph de dotNetRDF facilite la navigation dans les ontologies
  3. owl:equivalentClass declare la synonymie entre classes de vocabulaires différents
  4. ObjectProperty vs DatatypeProperty distingue les relations entre individus et vers les valeurs
  5. owl:FunctionalProperty et owl:inverseOf ajoutent des contraintes sur les proprietes
  6. Les profils OWL 2 (EL, QL, RL) offrent des compromis entre expressivite et performance
  7. SPARQL peut interroger et verifier les contraintes d’une ontologie OWL

RDFS vs OWL : recapitulatif comparatif

Capacite RDFS (SW-6) OWL 2 (SW-7)
Classes et hiérarchies Oui Oui + restrictions
Disjonction Non owl:disjointWith
Equivalence Non owl:equivalentClass
Proprietes inverses Non owl:inverseOf
Proprietes fonctionnelles Non owl:FunctionalProperty
Cardinalite Non owl:minCardinality, owl:maxCardinality
Logique decidable Limitee Garantie (selon profil)

Limites de OWL dans dotNetRDF

  • Pas de raisonneur OWL DL complet integre
  • StaticRdfsReasoner applique uniquement les règles RDFS
  • Pour un raisonnement OWL complet : utiliser HermiT (Glimm, Horrocks et al., HermiT: An OWL 2 Reasoner, J. Automated Reasoning 2014), Pellet (Sirin et al. 2007) ou FaCT++ (Tsarkov & Horrocks 2006) via un triple store

Prochaine étape

Dans le notebook suivant (SW-8-PythonRDF), nous decouvrirons : - La bibliotheque Python rdflib pour manipuler RDF - Comparaison de l’approche Python vs .NET - Interoperabilite des formats de serialisation

References

  • OWL 2 Web Ontology Language – Motik, Patel-Schneider & Cuenca Grau (eds.), W3C Recommendation (2009/2012). w3.org/TR/owl2-overview
  • OWL 2 Profiles – Motik et al., W3C Recommendation (2009). Sous-ensembles EL/QL/RL/DL.
  • The Description Logic Handbook – Baader, Calvanese, McGuinness, Nardi & Patel-Schneider (eds.), Cambridge University Press, 2e ed. (2007). Synthese de reference des logiques descriptives.
  • Pushing the EL Envelope (\(\mathcal{EL}^{++}\)) – Baader, Brandt & Lutz, IJCAI (2005). Logique du profil OWL 2 EL.
  • DL-Lite – Calvanese, De Giacomo, Lembo, Lenzerini & Rosati (2007). Logique du profil OWL 2 QL.
  • \(\mathcal{SROIQ}\) – Horrocks, Kutz & Sattler (2006). Logique du profil OWL 2 DL.
  • HermiT: An OWL 2 Reasoner – Glimm, Horrocks et al., Journal of Automated Reasoning (2014).
  • Pellet – Sirin et al., Journal of Web Semantics (2007). Raisonneur OWL 2.
  • FaCT++ – Tsarkov & Horrocks, DL workshop (2006). Raisonneur tableaux.

Navigation : << 6-RDFS | Index | 8-SHACL C# >>

Retour au sommet