/**************************************************************************** * @author John Miller * @version 1.0 * @date Sat Feb 20 21:00:25 EST 2010 * * This source file contains an object called Ontology and classes called * Concept (C), Role (R) and Individual (I). Together they provide methods * to implement ALC Description Logic (DL). ALC DL has the following five * operations: negation (-), conjunction (*), disjunction (+), all-values-from * (only) and some-values-from (some). * * Top-concept: T * Bottom-concept: _L * Negation: -C * Conjunction: C*D * Disjunction: C+D * Only: only R.C * Some: some R.C * * A DL Reasoner using a Tableaux Algorithm is provided. * Two test objects are included: * OntolgyTest: test cases involving students and courses * OntolgyTest2: test cases involving single digit numbers * @see http://www.inf.unibz.it/~franconi/dl/course/ */ import Ontology._ //import scala.collection.mutable.Set /**************************************************************************** * This object maintains global information about the ontology including sets * of all concepts, roles and individuals as well two predefined concepts: * the top (T) concept and the bottom (_L) concept. */ object Ontology { /** The set of all concepts */ var cALL = Set [Concept] () /** The set of all roles */ var rALL = Set [Role] () /** The set of all individuals */ var iALL = Set [Individual] () /** The top concept subsumes all concepts (ext* = all individuals) */ val T = Concept (List ("T")) /** The bottom concept is subsumed by all concepts (ext* = empty-set) */ val _L = Concept (List ("_L")) /************************************************************************ * Determine whether the ontology is consistent, i.e., every concept * except T and _L are satisfiable. */ def consistency (): Boolean = { var consistent = true for (c <- cALL if c != T && c != _L) { if (! Reasoner.sat (c)) { println ("Concept " + c + " is inconsistent") consistent = false } // if } // for consistent } // consistency /************************************************************************ * Classify the concepts by precisely determining sub and sup for each * concept. * FIX: complete the implementation */ def classify (): Boolean = { for (c <- cALL if c != T && c.sup.isEmpty) { c.sup += T; T.sub += c } // for true } // classify /************************************************************************ * Determine whether there is any individual in the ontology for which * i in c and i in -c. * FIX: complete the implementation */ def checkInstances (): Boolean = { for (c <- cALL if ! (c.ext & (-c).ext).isEmpty) { println ("Concept " + c + " has instances in its complement") return false } // for true } // checkInstances /************************************************************************ * Print the concept taxonomy. * @param c the current concept * @param level the depth of recursion */ def printTaxon (c: Concept, level: Int) { for (i <- 1 to level) print ("\t") println (c) for (d <- c.sub) printTaxon (d, level + 1) } // printTaxon } // Ontology /**************************************************************************** * This object implements a DL reasoner for ALC Description Logic using * a Tableaux Algorithm. */ object Reasoner { /** Widlcard */ val __ = "_" /** Pattern for conjunction> x1 : C * D */ val pat1 = List (__, ":", __, "*", __) /** Pattern for disjunction> x1 : C + D */ val pat2 = List (__, ":", __, "+", __) /** Pattern for some> x1 : some R . C */ val pat3 = List (__, ":", "some", __, ".", __) /** Pattern for only> x1 : only R . C */ val pat4 = List (__, ":", "only", __, ".", __) /** Pattern for not conjunction> - ( C * D ) */ val pat5 = List ("-", "(", __, "*", __, ")") /** Pattern for not disjunction> - ( C + D ) */ val pat6 = List ("-", "(", __, "+", __, ")") /** Pattern for not only> - ( only R . C ) */ val pat7 = List ("-", "(", "only", __, ".", __, ")") /** Pattern for not some> - ( some R . C ) */ val pat8 = List ("-", "(", "some", __, ".", __, ")") /** Counter for giving unique names to variables (x1, x2, x3, ...) */ var count = 0 /************************************************************************ * Determine whether the given concept c is satisfiable, i.e., is it * possible for c.ext to be non-empty. Turn the concept expression into * a list of constraints of the form (x : C), meaning that variable x * is a member of concept c. Apply Completion Rules (CR's) until either * a clash is found or no more rules can be applied. * @param c the given concept */ def sat (c: Concept): Boolean = { var constraints = List [List [String]] () // list of constraints var changes = true // change flag var cons = List [String] () // the matched constraint var cons_1 = List [String] () // derived constraint 1 var cons_2 = List [String] () // derived constraint 2 var pat_c = List [String] () // pattern for constraint count = 0 // reset the counter var x_i = newVar () // next variable name var i = 0 // match in ith constraint convert2NNF (c) // make sure c is in NNF constraints +:= List (x_i, ":") ++ c.e // add constraint for c.e while (changes) { changes = false i = locate (pat1, constraints) // CR1: conjunction pattern if (i >= 0) { // x1 : C * D println ("found " + pat1 + " in " + constraints + " at " + i) cons = constraints(i) cons_1 = List (cons(0), ":", cons(2)) // x1 : C cons_2 = List (cons(0), ":", cons(4)) // x1 : D // add derived constraints to list, unless they are there if (! (constraints contains cons_1)) { constraints :+= cons_1 changes = true } // if if (! (constraints contains cons_2)) { constraints :+= cons_2 changes = true } // if } // if i = locate (pat2, constraints) // CR2: disjunction pattern if (i >= 0) { // x1 : C + D println ("found " + pat2 + " in " + constraints + " at " + i) cons = constraints(i) cons_1 = List (cons(0), ":", cons(2)) // x1 : C cons_2 = List (cons(0), ":", cons(4)) // x1 : D // add derived constraints to list, unless they are there // if both are new, the constraint list must be branched // changes = true } // if i = locate (pat3, constraints) // CR3: some pattern if (i >= 0) { // x1 : some R . C println ("found " + pat3 + " in " + constraints + " at " + i) cons = constraints(i) x_i = newVar () cons_1 = List (cons(0), cons(3), x_i) // x1 R x2 pat_c = List (cons(0), cons(3), __) // correlated? if (locate (pat_c, constraints) < 0) { constraints :+= cons_1 changes = true } // if cons_2 = List (x_i, ":", cons(5)) // x2 : C pat_c = List (__, ":", cons(5)) // correlated? if (locate (pat_c, constraints) < 0) { constraints :+= cons_2 changes = true } // if } // if i = locate (pat4, constraints) // CR4: only pattern if (i >= 0) { // x1 : only R . C println ("found " + pat4 + " in " + constraints + " at " + i) cons = constraints(i) if (locate (List (cons(0), cons(3)), constraints) >= 0) { // need x1 R x2 // cons_1 = List () // x2 : C // add derived constraints to list, unless they are there // changes = true } // if } // if if (clash (constraints)) return false } // while true } // sat /************************************************************************ * Return the next variable name in the sequence: x1, x2, x3, ... */ def newVar (): String = { count += 1 "x" + count } // newVar /************************************************************************ * Convert concept c to Negation Normal Form (NNF). * @param c the given concept */ def convert2NNF (c: Concept) { var i = 0 var changes = true while (changes) { changes = false i = findAny (pat5, c.e) // - ( C * D ) - C + - D // println ("for " + pat5 + " found at " + i) if (i >= 0) { c.e = c.e.patch (i + 1, List (c.e(i + 2), "+" , "-", c.e(i + 4)), 5) changes = true } // if i = findAny (pat6, c.e) // - ( C + D ) - C * - D // println ("for " + pat6 + " found at " + i) if (i >= 0) { c.e = c.e.patch (i + 1, List (c.e(i + 2), "*" , "-", c.e(i + 4)), 5) changes = true } // if i = findAny (pat7, c.e) // - ( only R . C ) some R . - C // println ("for " + pat7 + " found at " + i) if (i >= 0) { c.e = c.e.patch (i, List ("some", c.e(i + 3), "." , "-", c.e(i + 5)), 7) changes = true } // if i = findAny (pat8, c.e) // - ( some R . C ) only R . - C // println ("for " + pat8 + " found at " + i) if (i >= 0) { c.e = c.e.patch (i, List ("only", c.e(i + 3), "." , "-", c.e(i + 5)), 7) changes = true } // if } // while } // convert2NNF /************************************************************************ * Determine which constraint (if any) matches the pattern 'pat'. * @param pat the pattern to match * @param constraints the set of constraints */ def locate (pat: List [String], constraints: List [List [String]]): Int = { for (i <- 0 until constraints.size if find (pat, constraints(i))) return i -1 // no match found } // locate /************************************************************************ * Determine whether the pattern matches a prefix of the expression. * @param pat the pattern to match * @param expr the expression to examine */ def find (pat: List [String], expr: List [String]): Boolean = { if (expr.size < pat.size) return false for (j <- 0 until pat.size if pat(j) != __ && pat(j) != expr(j)) return false true } // find /************************************************************************ * Determine where (if any) the pattern matches the expression. * @param pat the pattern to match * @param expr the expression to examine */ def findAny (pat: List [String], expr: List [String]): Int = { if (expr.size < pat.size) return -1 // match not found for (i <- 0 to expr.size - pat.size) { var found = true for (j <- 0 until pat.size if pat(j) != __ && pat(j) != expr(i + j)) found = false if (found) return i // match found starting at position i } // for -1 // match not found } // findAny /************************************************************************ * Determine whether there is a constradiction/clash in the constraints. * @param constraints the set of constraints */ def clash (constraints: List [List [String]]): Boolean = { println ("clash: look for a clash in these constraints:\n" + constraints) // FIX THIS IMPLEMENTATION false } // clash } // Reasoner /**************************************************************************** * This class is used to represent concepts. A concept has an intention * (or meaning) and an extension (or set of individuals that are members). * @param e the defining concept expression/formula */ case class Concept (var e: List [String]) { { cALL += this } // primary constructor /** The extension or set of individuals that are members */ var ext = Set [Individual] () /** The set of directly subsumed concepts (children) */ var sub = Set [Concept] () /** The set of directly subsuming concepts (parents) */ var sup = Set [Concept] () /************************************************************************ * Compute the negation/complement of this concept. */ def unary_- : Concept = { val c = Concept ( if (e == T.e) _L.e else if (e == _L.e) T.e else if (e.head == "-") e.tail else List ("-") ++ e) c.ext = iALL -- ext c } // - /************************************************************************ * Compute the conjunction/intersection of this concept with that concept d. * @param d that concept */ def * (d: Concept): Concept = { val c = Concept ( if (e == T.e) d.e else if (d.e == T.e) e else if (e == _L.e) _L.e else if (d.e == _L.e) _L.e else if (e == (-d).e) _L.e else (e :+ "*") ++ d.e ) c subsumedBy this c subsumedBy d c.ext = ext & d.ext c } // * /************************************************************************ * Compute the disjunction/union of this concept with that concept d. * @param d that concept */ def + (d: Concept): Concept = { val c = Concept ( if (e == T.e) T .e else if (d.e == T.e) T.e else if (e == _L.e) d.e else if (d.e == _L.e) e else if (e == (-d).e) T.e else (e :+ "+") ++ d.e) this subsumedBy c d subsumedBy c c.ext = ext ++ d.ext c } // + /************************************************************************ * Put backets () around the concept: c -> ( c ). */ def bracket: Concept = { val c = Concept (List ("(") ++ e :+ ")") c equiv this c.ext = ext c } // bracket /************************************************************************ * Determine whether this concept c is 'subsumed-by' that concept d, i.e., * c <= d implies c.ext subsetOf d.ext. * Note, c is the subsumee and d is the subsumer (more general concept) * To ensure accuracy, first use the reasoner to classify the concepts. * @param d that concept */ def <= (d: Concept): Boolean = { supStar contains d } // <= /************************************************************************ * Specify that this concept c is 'subsumed-by' that concept d, i.e., * c subsumedBy d implies c.ext subsetOf d.ext. * Note, c is the subsumee and d is the subsumer (more general concept) * @param d that concept */ def subsumedBy (d: Concept) { sup += d // make d a parent of this d.sub += this // make this a child of d } // subsumedBy /************************************************************************ * Specify that this concept c is 'equivalent-to' that concept d, i.e., * c equiv d implies c.ext = d.ext. * @param d that concept */ def equiv (d: Concept) { this subsumedBy d d subsumedBy this } // equiv /************************************************************************ * Compute the transitive closure of sub (sub*, the descendants). */ def subStar: Set [Concept] = { var desc = Set [Concept] () desc ++= sub for (d <- desc) desc ++= d.subStar desc } // subStar /************************************************************************ * Compute the transitive closure of sup (sup*, the ancestors). */ def supStar: Set [Concept] = { var anc = Set [Concept] () anc ++= sup for (d <- anc) anc ++= d.supStar anc } // supStar /************************************************************************ * Retrieve all individuals from this concept and its descendants (ext*). */ def extStar: Set [Individual] = { var extAll = Set [Individual] () extAll ++= ext for (d <- subStar) extAll ++= d.ext extAll } // extStar /************************************************************************ * Add individual(s) to the extension (ext) of this concept. * @param i the individual(s) to add */ def add (i: Individual*) { for (j <- 0 until i.length) ext += i(j) } // add /************************************************************************ * Convert this concept to its string representation. */ override def toString: String = { var ss = "Concept (" for (s <- e) ss += " " + s ss + " )\t\t ext = " + ext } // toString } // Concept class /**************************************************************************** * This class is used to represent roles. A role has an intention * (or meaning) and an extension (or set of ordered pairs of individuals). * R can also be viewed as a property: (x, y) in r means x.r = y */ case class Role (n: String) { rALL += this /** Ordered pairs of individuals {(first-individual, second-individual)} */ var pairs = Set [Tuple2 [Individual, Individual]] () /************************************************************************ * Retrieve the domain of r: {first-individual} */ def domain: Set [Individual] = { var xSet = Set [Individual] () for (p <- pairs) xSet += p._1 xSet } // domain /************************************************************************ * Retrieve the range of r: {second-individual} */ def range: Set [Individual] = { var ySet = Set [Individual] () for (p <- pairs) ySet += p._2 ySet } // range /************************************************************************ * Compute the universal quantification (only) of this role r applied to * concept d. Return a derived concept c whose extension is such that * x in c iff for-all y where (x, y) in r implies y in d. */ def only (d: Concept): Concept = { val c = Concept (List ("only") ++ List (n) ++ List (".") ++ d.e) c.ext = iALL for (p <- pairs if ! (d.ext contains p._2)) c.ext -= p._1 c } // only /************************************************************************ * Compute the existential quantification (some) of this role r applied * to concept d. Return a derived concept c whose extension is such that * x in c iff there-is y where (x, y) in r and y in d. */ def some (d: Concept): Concept = { val c = Concept (List ("some") ++ List (n) ++ List (".") ++ d.e) for (p <- pairs if d.ext contains p._2) c.ext += p._1 c } // some /************************************************************************ * Add a pair of individuals to the extension of this role. * @param i the first individual of the pair to add * @param j the second individual of the pair to add */ def add (i_j: Tuple2 [Individual, Individual]*) { for (k <- 0 until i_j.length) pairs += i_j(k) } // add /************************************************************************ * Convert this concept to its string representation. */ override def toString: String = { "Role ( " + n + " )\t\t pairs = " + pairs } // toString } // Role class /**************************************************************************** * This class is represent individuals. An individual may be viewed as an * entity that can exist on its own and may be a member of one or more * concepts as well as have one or more roles. * @param n the name of the individual */ case class Individual (n: String) { iALL += this /************************************************************************ * Convert this concept to its string representation. */ override def toString: String = { n } // toString } // Individual class /**************************************************************************** * Test1: This object is used to test the Concept, Role and Individual classes. * The test cases involve students and courses. */ object OntologyTest extends Application { // Create Concepts // val pers = Concept (List ("Person")) val stud = Concept (List ("Student")) val grad = Concept (List ("Graduate")) val teac = Concept (List ("Teacher")) val cour = Concept (List ("Course")) val club = Concept (List ("Club")) stud subsumedBy pers // subsumption // Create Roles // val enro = Role ("Enrolled") // Create Individuals // val john = Individual ("john"); stud.add (john) val mary = Individual ("mary"); stud.add (mary); grad.add (mary) val mike = Individual ("mike"); teac.add (mike) val chem = Individual ("chem"); cour.add (chem) val golf = Individual ("golf"); club.add (golf) val bart = Individual ("bart"); enro.add ((bart, chem)) val fred = Individual ("fred"); enro.add ((fred, golf)) val walt = Individual ("walt"); enro.add ((walt, chem)); enro.add ((walt, golf)) // Print Concepts // println ("\nPrint Concepts") println ("stud = " + stud) println ("grad = " + grad) println ("teac = " + teac) println ("cour = " + cour) println ("club = " + club) // Print Roles // println ("\nPrint Roles") println ("enro = " + enro) // Print Derived Concepts // println ("\nPrint Derived Concepts") println ("stud * grad = " + (stud * grad)) println ("stud * -grad = " + (stud * -grad)) println ("- (-teac) = " + (- (-teac))) println ("teac * -teac = " + (teac * -teac)) println ("enro.some (cour) = " + enro.some (cour)) println ("enro.only (cour) = " + enro.only (cour)) // Print Taxonomy // println ("\nPrint Taxonomy") if (classify ()) printTaxon (T, 0) // Test NNF // println ("\nTest NNF") val c = Concept (List ("C")) val d = Concept (List ("D")) val r = Role ("R") val f1 = -(c * d).bracket val f2 = -(c + d).bracket val f3 = -(r.only (c)).bracket val f4 = -(r.some (c)).bracket println ("convert " + f1); Reasoner.convert2NNF (f1); println (" to NNF: " + f1) println ("convert " + f2); Reasoner.convert2NNF (f2); println (" to NNF: " + f2) println ("convert " + f3); Reasoner.convert2NNF (f3); println (" to NNF: " + f3) println ("convert " + f4); Reasoner.convert2NNF (f4); println (" to NNF: " + f4) // Test Satisfiability // println ("\nTest Satisfiability") val f5 = c * d val f6 = r.some (c) val f7 = (r.some (c)).bracket * (r.some (-c)).bracket println ("--------------------------------------------------") println ("Reasoner.sat ( " + f5 + " ) = " + Reasoner.sat (f5)) println ("--------------------------------------------------") println ("Reasoner.sat ( " + f6 + " ) = " + Reasoner.sat (f6)) println ("--------------------------------------------------") println ("Reasoner.sat ( " + f7 + " ) = " + Reasoner.sat (f7)) println ("--------------------------------------------------") } // OntologyTest object /**************************************************************************** * Test2: This object is used to test the Concept, Role and Individual classes. * The test cases involve single digit numbers. */ object OntologyTest2 extends Application { val _1 = Individual ("1") val _2 = Individual ("2") val _3 = Individual ("3") val _4 = Individual ("4") val _5 = Individual ("5") val _6 = Individual ("6") val _7 = Individual ("7") val _8 = Individual ("8") val _9 = Individual ("9") val even = Concept (List ("Even")); even.add (_2, _4, _6, _8) val square = Concept (List ("Square")); square.add (_1, _4, _9) val oddSquare = -even * square val lessThan = Role ("LessThan") lessThan.add ((_1, _2), (_1, _3), (_1, _4), (_1, _5), (_1, _6), (_1, _7), (_1, _8), (_1, _9), (_2, _3), (_2, _4), (_2, _5), (_2, _6), (_2, _7), (_2, _8), (_2, _9), (_3, _4), (_3, _5), (_3, _6), (_3, _7), (_3, _8), (_3, _9), (_4, _5), (_4, _6), (_4, _7), (_4, _8), (_4, _9), (_5, _6), (_5, _7), (_5, _8), (_5, _9), (_6, _7), (_6, _8), (_6, _9), (_7, _8), (_7, _9), (_8, _9)) println ("even = " + even) println ("square = " + square) println ("oddSquare = " + oddSquare) println ("lessThan.some (square) = " + lessThan.some (square)) println ("lessThan.only (square) = " + lessThan.only (square)) } // OntologyTest2 object