From e932527e33b6f4c1634995224188b26d870d92b2 Mon Sep 17 00:00:00 2001 From: Federico Igne Date: Mon, 31 May 2021 15:06:47 +0100 Subject: Add scafolding for generic approximation support --- .../scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala | 305 +++++++++++---------- .../cs/rsacomb/approximation/approximation.scala | 16 ++ .../ox/cs/rsacomb/approximation/lowerbound.scala | 207 ++++++++++++++ 3 files changed, 385 insertions(+), 143 deletions(-) create mode 100644 src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala create mode 100644 src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala (limited to 'src') diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala index c7b3bf0..e048c28 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala @@ -64,6 +64,104 @@ import uk.ac.ox.cs.rsacomb.sparql._ import uk.ac.ox.cs.rsacomb.util.{RDFoxUtil, RSA} import uk.ac.ox.cs.rsacomb.util.Logger +object RSAUtil { + + implicit def axiomsToOntology(axioms: Seq[OWLAxiom]) = { + val manager = OWLManager.createOWLOntologyManager() + manager.createOntology(axioms.asJava) + } + + /** Compute the RSA dependency graph for a set of axioms + * + * @return a tuple containing the dependency graph and a map between + * the newly introduced constants and the corresponding input axioms. + * + * @note no check on the ontology language is performed since the + * construction of the dependency graph is computed regardless. The + * input axioms are assumed to be normalized. + */ + private def dependencyGraph( + axioms: Seq[OWLAxiom], + datafiles: Seq[File] + ): (Graph[Resource, DiEdge], Map[String, OWLAxiom]) = { + val unsafe = this.unsafeRoles + var nodemap = Map.empty[String, OWLAxiom] + + object RSAConverter extends RDFoxConverter { + + override def convert( + expr: OWLClassExpression, + term: Term, + unsafe: List[OWLObjectPropertyExpression], + skolem: SkolemStrategy, + suffix: RSASuffix + ): Shards = + (expr, skolem) match { + + case (e: OWLObjectSomeValuesFrom, c: Constant) => { + nodemap.update(c.iri.getIRI, c.axiom) + val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) + if (unsafe contains e.getProperty) + (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) + else + (RSA.PE(term, c.iri) :: res, ext) + } + + case (e: OWLDataSomeValuesFrom, c: Constant) => { + nodemap.update(c.iri.getIRI, c.axiom) + val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) + if (unsafe contains e.getProperty) + (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) + else + (RSA.PE(term, c.iri) :: res, ext) + } + + case _ => super.convert(expr, term, unsafe, skolem, suffix) + } + } + + /* Ontology convertion into LP rules */ + val term = RSAOntology.genFreshVariable() + val result = axioms.map(a => + RSAConverter.convert(a, term, unsafe, new Constant(a), Empty) + ) + + val datalog = result.unzip + val facts = datalog._1.flatten + var rules = datalog._2.flatten + + /* Open connection with RDFox */ + val (server, data) = RDFoxUtil.openConnection("rsa_dependency_graph") + + /* Add additional built-in rules */ + val varX = Variable.create("X") + val varY = Variable.create("Y") + rules = Rule.create( + RSA.E(varX, varY), + RSA.PE(varX, varY), + RSA.U(varX), + RSA.U(varY) + ) :: rules + /* Load facts and rules from ontology */ + RDFoxUtil.addFacts(data, facts) + RDFoxUtil.addRules(data, rules) + /* Load data files */ + RDFoxUtil.addData(data, datafiles: _*) + + /* Build the graph */ + val query = "SELECT ?X ?Y WHERE { ?X rsa:E ?Y }" + val answers = RDFoxUtil.submitQuery(data, query, RSA.Prefixes).get + var edges: Seq[DiEdge[Resource]] = + answers.collect { case (_, Seq(n1, n2)) => n1 ~> n2 } + val graph = Graph(edges: _*) + + /* Close connection to RDFox */ + RDFoxUtil.closeConnection(server, data) + + (graph, nodemap) + } +} + object RSAOntology { /** Name of the RDFox data store used for CQ answering */ @@ -79,14 +177,63 @@ object RSAOntology { /** Manager instance to interface with OWLAPI */ val manager = OWLManager.createOWLOntologyManager() - def apply(ontology: File, data: File*): RSAOntology = + def apply( + ontofile: File, + datafiles: Seq[File], + approx: Option[Approximation] = None + ): RSAOntology = { + val ontology = manager.loadOntologyFromOntologyDocument(ontofile) + RSAOntology(ontology, datafiles, approx) + } + + def apply( + ontology: OWLOntology, + datafiles: Seq[File], + approx: Option[Approximation] = None + ): RSAOntology = { + val normalizer = new Normalizer() + + /** TBox axioms */ + var tbox: List[OWLLogicalAxiom] = + original + .tboxAxioms(Imports.INCLUDED) + .collect(Collectors.toList()) + .collect { case a: OWLLogicalAxiom => a } + .flatMap(normalizer.normalize) + + /** RBox axioms */ + var rbox: List[OWLLogicalAxiom] = + original + .rboxAxioms(Imports.INCLUDED) + .collect(Collectors.toList()) + .collect { case a: OWLLogicalAxiom => a } + .flatMap(normalizer.normalize) + + /** ABox axioms + * + * @note this represents only the set of assertions contained in the + * ontology file. Data files specified in `datafiles` are directly + * imported in RDFox due to performance issues when trying to import + * large data files via OWLAPI. + */ + var abox: List[OWLLogicalAxiom] = + original + .aboxAxioms(Imports.INCLUDED) + .collect(Collectors.toList()) + .collect { case a: OWLLogicalAxiom => a } + .flatMap(normalizer.normalize) + + /** Collection of logical axioms in the input ontology */ + var axioms: List[OWLLogicalAxiom] = abox ::: tbox ::: rbox + new RSAOntology( - manager.loadOntologyFromOntologyDocument(ontology), - data: _* + approx match { + case Some(a) => a.approximate(axioms, datafiles) + case None => axioms + }, + datafiles ) - - def apply(ontology: OWLOntology, data: File*): RSAOntology = - new RSAOntology(ontology, data: _*) + } } /** Wrapper class for an ontology in RSA @@ -94,7 +241,7 @@ object RSAOntology { * @param ontology the input OWL2 ontology. * @param datafiles additinal data (treated as part of the ABox) */ -class RSAOntology(val original: OWLOntology, val datafiles: File*) { +class RSAOntology(val axioms: Seq[OWLAxiom], val datafiles: File*) { /** Simplify conversion between OWLAPI and RDFox concepts */ import implicits.RDFox._ @@ -104,49 +251,8 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) { /** Set of axioms removed during the approximation to RSA */ private var removed: Seq[OWLAxiom] = Seq.empty - /** The normalizer normalizes the ontology and approximate it to - * Horn-ALCHOIQ. A further step is needed to obtain an RSA - * approximation of the input ontology `original`. - */ - private val normalizer = new Normalizer() - - /** TBox axioms */ - var tbox: List[OWLLogicalAxiom] = - original - .tboxAxioms(Imports.INCLUDED) - .collect(Collectors.toList()) - .collect { case a: OWLLogicalAxiom => a } - .flatMap(normalizer.normalize) - - /** RBox axioms */ - var rbox: List[OWLLogicalAxiom] = - original - .rboxAxioms(Imports.INCLUDED) - .collect(Collectors.toList()) - .collect { case a: OWLLogicalAxiom => a } - .flatMap(normalizer.normalize) - - /** ABox axioms - * - * @note this represents only the set of assertions contained in the - * ontology file. Data files specified in `datafiles` are directly - * imported in RDFox due to performance issues when trying to import - * large data files via OWLAPI. - */ - var abox: List[OWLLogicalAxiom] = - original - .aboxAxioms(Imports.INCLUDED) - .collect(Collectors.toList()) - .collect { case a: OWLLogicalAxiom => a } - .flatMap(normalizer.normalize) - - /** Collection of logical axioms in the input ontology */ - var axioms: List[OWLLogicalAxiom] = abox ::: tbox ::: rbox - /** Normalized Horn-ALCHOIQ ontology */ - val ontology = RSAOntology.manager.createOntology( - axioms.asInstanceOf[List[OWLAxiom]].asJava - ) + val ontology = RSAOntology.manager.createOntology(axioms.asJava) /** OWLAPI internal reasoner instantiated over the approximated ontology */ private val reasoner = @@ -170,7 +276,7 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) { val concepts: List[OWLClass] = ontology.getClassesInSignature().asScala.toList val roles: List[OWLObjectPropertyExpression] = - (tbox ++ rbox) + axioms .flatMap(_.objectPropertyExpressionsInSignature) .distinct @@ -190,12 +296,12 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) { /* Checking for unsafety condition (1) */ val unsafe1 = for { - axiom <- tbox + axiom <- axioms if axiom.isT5 role1 <- axiom.objectPropertyExpressionsInSignature roleSuper = role1 +: reasoner.superObjectProperties(role1) roleSuperInv = roleSuper.map(_.getInverseProperty) - axiom <- tbox + axiom <- axioms if axiom.isT3 && !axiom.isT3top role2 <- axiom.objectPropertyExpressionsInSignature if roleSuperInv contains role2 @@ -203,12 +309,12 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) { /* Checking for unsafety condition (2) */ val unsafe2 = for { - axiom <- tbox + axiom <- axioms if axiom.isT5 role1 <- axiom.objectPropertyExpressionsInSignature roleSuper = role1 +: reasoner.superObjectProperties(role1) roleSuperInv = roleSuper.map(_.getInverseProperty) - axiom <- tbox + axiom <- axioms if axiom.isT4 role2 <- axiom.objectPropertyExpressionsInSignature if roleSuper.contains(role2) || roleSuperInv.contains(role2) @@ -217,93 +323,6 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) { unsafe1 ++ unsafe2 } - /** Compute the RSA dependency graph - * - * This is used to approximate the input ontology to RSA. - * - * @return a tuple containing the dependency graph and a map between - * the constants newly introduced and the corresponding axioms in the - * ontology. - */ - private def dependencyGraph() - : (Graph[Resource, DiEdge], Map[String, OWLAxiom]) = { - val unsafe = this.unsafeRoles - var nodemap = Map.empty[String, OWLAxiom] - - object RSAConverter extends RDFoxConverter { - - override def convert( - expr: OWLClassExpression, - term: Term, - unsafe: List[OWLObjectPropertyExpression], - skolem: SkolemStrategy, - suffix: RSASuffix - ): Shards = - (expr, skolem) match { - - case (e: OWLObjectSomeValuesFrom, c: Constant) => { - nodemap.update(c.iri.getIRI, c.axiom) - val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) - if (unsafe contains e.getProperty) - (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) - else - (RSA.PE(term, c.iri) :: res, ext) - } - - case (e: OWLDataSomeValuesFrom, c: Constant) => { - nodemap.update(c.iri.getIRI, c.axiom) - val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) - if (unsafe contains e.getProperty) - (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) - else - (RSA.PE(term, c.iri) :: res, ext) - } - - case _ => super.convert(expr, term, unsafe, skolem, suffix) - } - } - - /* Ontology convertion into LP rules */ - val term = RSAOntology.genFreshVariable() - val result = axioms.map(a => - RSAConverter.convert(a, term, unsafe, new Constant(a), Empty) - ) - - val datalog = result.unzip - val facts = datalog._1.flatten - var rules = datalog._2.flatten - - /* Open connection with RDFox */ - val (server, data) = RDFoxUtil.openConnection("rsa_dependency_graph") - - /* Add additional built-in rules */ - val varX = Variable.create("X") - val varY = Variable.create("Y") - rules = Rule.create( - RSA.E(varX, varY), - RSA.PE(varX, varY), - RSA.U(varX), - RSA.U(varY) - ) :: rules - /* Load facts and rules from ontology */ - RDFoxUtil.addFacts(data, facts) - RDFoxUtil.addRules(data, rules) - /* Load data files */ - RDFoxUtil.addData(data, datafiles: _*) - - /* Build the graph */ - val query = "SELECT ?X ?Y WHERE { ?X rsa:E ?Y }" - val answers = RDFoxUtil.submitQuery(data, query, RSA.Prefixes).get - var edges: Seq[DiEdge[Resource]] = - answers.collect { case (_, Seq(n1, n2)) => n1 ~> n2 } - val graph = Graph(edges: _*) - - /* Close connection to RDFox */ - RDFoxUtil.closeConnection(server, data) - - (graph, nodemap) - } - /** Approximate a Horn-ALCHOIQ ontology to RSA * * This is done by gathering those axioms that prevent the ontology @@ -653,7 +672,7 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) { val conflR = this.confl(roleR) // We just need the TBox to find val terms = for { - axiom1 <- tbox + axiom1 <- axioms if axiom1.isT5 // We expect only one role coming out of a T5 axiom roleS <- axiom1.objectPropertyExpressionsInSignature @@ -693,7 +712,7 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) { ) Logger.print( s"Logical axioms in Horn-ALCHOIQ ontology: ${ontology - .getLogicalAxiomCount(true)} (${tbox.length}/${rbox.length}/${abox.length})", + .getLogicalAxiomCount(true)} (${axioms.length}/${axioms.length}/${axioms.length})", level ) Logger.print( diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala new file mode 100644 index 0000000..073d0d9 --- /dev/null +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala @@ -0,0 +1,16 @@ +package uk.ac.ox.cs.rsacomb.approximation + +import java.io.File +import org.semanticweb.owlapi.model.OWLAxiom + +/** Ontology approximation technique. */ +trait Approximation { + + /** Approximate an ontology. + * + * @param ontology input ontology + * @return a new approximated ontology + */ + def approximate(ontology: Seq[OWLAxiom], datafiles: Seq[File]): Seq[OWLAxiom] + +} diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala new file mode 100644 index 0000000..8036250 --- /dev/null +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala @@ -0,0 +1,207 @@ +package uk.ac.ox.cs.rsacomb.approximation + +/** Approximation algorithm that mantains soundness for CQ answering. + * + * The input OWL 2 ontology is assumed to be normalized and the output + * ontology is guaranteed to be in RSA. + * + * The algorithm is performed in three steps: + * 1. the ontology is reduced to ALCHOIQ by discarding any axiom + * that is not in the language; + * 2. the ontology is further reduced to Horn-ALCHOIQ by shifting + * axioms with disjunction on the rhs; + * 3. the ontology is approximated to RSA by manipulating its + * dependency graph. + * + * @see [[uk.ac.ox.cs.rsacomb.converter.Normalizer]] + */ +class LowerBound extends Approximation { + + val normalizer = new Normalizer() + + /** Main entry point for the approximation algorithm */ + def approximate( + ontology: Seq[OWLAxiom], + datafiles: Seq[File] + ): Seq[OWLAxiom] = { + /* Normalize axioms */ + val axioms1 = axioms flatMap normalizer.normalize + /* Delete any axiom outside of ALCHOIQ */ + val axioms2 = axioms1 filterNot inHornLACHOIQ + /* Shift any axiom with disjunction on the rhs */ + val axioms3 = for { + a1 <- axioms1 + a2 <- shift(a1) + a3 <- normalize(a2) + } yield a3 + /* Approximate to RSA */ + toRSA(axioms3, datafiles) + } + + /** Discards all axioms outside ALCHOIQ */ + def inALCHOIQ(axiom: OWLLogicalAxiom): Boolean = + axiom match { + case a: OWLSubClassOfAxiom => { + val sub = a.getSubClass.getNNF + val sup = a.getSuperClass.getNNF + (sub, sup) match { + case (sub: OWLObjectAllValuesFrom, _) => false + case (sub: OWLDataAllValuesFrom, _) => false + case (_, sup: OWLDataAllValuesFrom) => false + case (sub: OWLObjectMinCardinality, _) if sub.getCardinality >= 2 => + false + case (sub: OWLDataMinCardinality, _) if sub.getCardinality >= 2 => + false + case (_, sup: OWLObjectMinCardinality) if sup.getCardinality >= 2 => + false + case (_, sup: OWLDataMinCardinality) if sup.getCardinality >= 2 => + false + case (sub: OWLObjectMaxCardinality, _) => false + case (sub: OWLDataMaxCardinality, _) => false + case (_, sup: OWLObjectMaxCardinality) if sup.getCardinality >= 2 => + false + case (_, sup: OWLDataMaxCardinality) if sup.getCardinality >= 1 => + false + case (_, sup: OWLObjectOneOf) if sup.getIndividuals.length > 2 => + false + case (sub: OWLObjectHasSelf, _) => false + case (_, sup: OWLObjectHasSelf) => false + case _ => true + } + } + case a: OWLTransitiveObjectPropertyAxiom => false + case a: OWLReflexiveObjectPropertyAxiom => false + case a: OWLSubPropertyChainOfAxiom => false + case a: OWLAsymmetricObjectPropertyAxiom => false + case a => true + } + + /** Shifting axioms with disjunction on the rhs. + * + * The process of shifting presenves soundness but completenes w.r.t. + * CQ answering is lost. + * + * @example + * + * A -> B1 u B2 u B3 . + * + * becomes + * + * A n nB1 n nB2 -> B3 . + * A n nB1 n nB3 -> B2 . + * A n nB2 n nB3 -> B1 . + * nB1 n nB2 n nB3 -> nA . + * + * where nA, nB1, nB2, nB3 are fresh predicates "corresponding" to + * the negation of A, B1, B2, B3 respectively. + */ + def shift(axiom: OWLLogicalAxiom): Seq[OWLLogicalAxiom] = + axiom match { + case a: OWLSubClassOfAxiom => { + val sub = a.getSubClass.getNNF + val sup = a.getSuperClass.getNNF + sup match { + case sup: OWLObjectUnionOf => { + val body = sub.asConjunctSet.map((atom) => (atom, freshOWLClass())) + val head = sup.asDisjunctSet.map((atom) => (atom, freshOWLClass())) + + val r1 = + factory.getOWLSubClassOfAxiom( + factory.getOWLObjectIntersectionOf( + (body.map(_._1) ++ head.map(_._2)): _* + ), + factory.getOWLNothing + ) + + val r2s = + for { + (a, na) <- head + hs = head.map(_._2).filterNot(_ equals na) + } yield factory.getOWLSubClassOfAxiom( + factory.getOWLObjectIntersectionOf( + (body.map(_._1) ++ hs): _* + ), + a + ) + + val r3s = + for { + (a, na) <- body + bs = body.map(_._1).filterNot(_ equals a) + } yield factory.getOWLSubClassOfAxiom( + factory.getOWLObjectIntersectionOf( + (bs ++ head.map(_._2)): _* + ), + na + ) + + Seq(r1) ++ r2s ++ r3s + } + case _ => Seq(axiom) + } + } + case _ => Seq(axiom) + } + + /** Approximate a Horn-ALCHOIQ ontology to RSA + * + * This is done by gathering those axioms that prevent the ontology + * dependency graph from being tree-shaped, and removing them. + * + * @param axioms the set of axioms to approximate. + * @return the approximated set of axioms. + */ + def toRSA(axioms: Seq[OWLAxiom], datafiles: Seq[File]): Seq[OWLAxiom] = { + /* Compute the dependency graph for the ontology */ + val (graph, nodemap) = RSAUtil.dependencyGraph(axioms, datafiles) + + /* Define node colors for the graph visit */ + sealed trait NodeColor + case object Unvisited extends NodeColor + case object Visited extends NodeColor + case object ToDelete extends NodeColor + + /* Keep track of node colors during graph visit */ + var color = Map.from[Resource, NodeColor]( + graph.nodes.toOuter.map(k => (k, Unvisited)) + ) + + for { + component <- graph.componentTraverser().map(_ to Graph) + edge <- component + .outerEdgeTraverser(component.nodes.head) + .withKind(BreadthFirst) + } yield { + val source = edge._1 + val target = edge._2 + color(source) match { + case Unvisited | Visited => { + color(target) match { + case Unvisited => + color(source) = Visited; + color(target) = Visited + case Visited => + color(source) = ToDelete + case ToDelete => + color(source) = Visited + } + } + case ToDelete => + } + } + + val toDelete = color.iterator.collect { case (resource: IRI, ToDelete) => + nodemap(resource.getIRI) + }.toSeq + + /* Remove axioms from approximated ontology */ + axioms diff toDelete + } + // val edges1 = Seq('A ~> 'B, 'B ~> 'C, 'C ~> 'D, 'D ~> 'H, 'H ~> + // 'G, 'G ~> 'F, 'E ~> 'A, 'E ~> 'F, 'B ~> 'E, 'F ~> 'G, 'B ~> 'F, + // 'C ~> 'G, 'D ~> 'C, 'H ~> 'D) + // val edges2 = Seq('I ~> 'M, 'I ~> 'L, 'L ~> 'N, 'M ~> 'N) + // val edges3 = Seq('P ~> 'O) + // val graph = Graph.from(edges = edges1 ++ edges2 ++ edges3) + // +} -- cgit v1.2.3 From 5f9815c3f67114645593840a8648bffb1207b8d0 Mon Sep 17 00:00:00 2001 From: Federico Igne Date: Mon, 31 May 2021 16:58:15 +0100 Subject: Multiple fixes --- src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala | 4 +- .../scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala | 88 ++++++++++++---------- .../cs/rsacomb/approximation/approximation.scala | 7 +- .../ox/cs/rsacomb/approximation/lowerbound.scala | 35 ++++++--- 4 files changed, 80 insertions(+), 54 deletions(-) (limited to 'src') diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala index 74084af..a807f75 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala @@ -108,10 +108,10 @@ object RSAComb extends App { val ontology = RSAOntology( config('ontology).get[File], - config('data).get[List[File]]: _* + config('data).get[List[File]], + None ) val rsa = ontology.toRSA() - ontology.statistics() if (config contains 'query) { val query = diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala index e048c28..4d0f13d 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala @@ -50,13 +50,13 @@ import scala.collection.JavaConverters._ import scala.collection.mutable.{Set, Map} import scalax.collection.Graph import scalax.collection.GraphPredef._, scalax.collection.GraphEdge._ -import scalax.collection.GraphTraversal._ /* Debug only */ import org.semanticweb.owlapi.dlsyntax.renderer.DLSyntaxObjectRenderer import tech.oxfordsemantic.jrdfox.logic._ import org.semanticweb.owlapi.model.OWLObjectInverseOf +import uk.ac.ox.cs.rsacomb.approximation.Approximation import uk.ac.ox.cs.rsacomb.converter._ import uk.ac.ox.cs.rsacomb.filtering.{FilteringProgram, FilterType} import uk.ac.ox.cs.rsacomb.suffix._ @@ -66,10 +66,10 @@ import uk.ac.ox.cs.rsacomb.util.Logger object RSAUtil { - implicit def axiomsToOntology(axioms: Seq[OWLAxiom]) = { - val manager = OWLManager.createOWLOntologyManager() - manager.createOntology(axioms.asJava) - } + // implicit def axiomsToOntology(axioms: Seq[OWLAxiom]) = { + // val manager = OWLManager.createOWLOntologyManager() + // manager.createOntology(axioms.asJava) + // } /** Compute the RSA dependency graph for a set of axioms * @@ -81,10 +81,10 @@ object RSAUtil { * input axioms are assumed to be normalized. */ private def dependencyGraph( - axioms: Seq[OWLAxiom], - datafiles: Seq[File] + axioms: List[OWLLogicalAxiom], + datafiles: List[File] ): (Graph[Resource, DiEdge], Map[String, OWLAxiom]) = { - val unsafe = this.unsafeRoles + val unsafe = RSAOntology(axioms, datafiles).unsafeRoles var nodemap = Map.empty[String, OWLAxiom] object RSAConverter extends RDFoxConverter { @@ -164,6 +164,8 @@ object RSAUtil { object RSAOntology { + import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ + /** Name of the RDFox data store used for CQ answering */ private val DataStore = "answer_computation" @@ -177,10 +179,15 @@ object RSAOntology { /** Manager instance to interface with OWLAPI */ val manager = OWLManager.createOWLOntologyManager() + def apply( + axioms: List[OWLLogicalAxiom], + datafiles: List[File] + ): RSAOntology = new RSAOntology(axioms, datafiles: _*) + def apply( ontofile: File, - datafiles: Seq[File], - approx: Option[Approximation] = None + datafiles: List[File], + approx: Option[Approximation] ): RSAOntology = { val ontology = manager.loadOntologyFromOntologyDocument(ontofile) RSAOntology(ontology, datafiles, approx) @@ -188,14 +195,14 @@ object RSAOntology { def apply( ontology: OWLOntology, - datafiles: Seq[File], - approx: Option[Approximation] = None + datafiles: List[File], + approx: Option[Approximation] ): RSAOntology = { val normalizer = new Normalizer() /** TBox axioms */ var tbox: List[OWLLogicalAxiom] = - original + ontology .tboxAxioms(Imports.INCLUDED) .collect(Collectors.toList()) .collect { case a: OWLLogicalAxiom => a } @@ -203,7 +210,7 @@ object RSAOntology { /** RBox axioms */ var rbox: List[OWLLogicalAxiom] = - original + ontology .rboxAxioms(Imports.INCLUDED) .collect(Collectors.toList()) .collect { case a: OWLLogicalAxiom => a } @@ -217,7 +224,7 @@ object RSAOntology { * large data files via OWLAPI. */ var abox: List[OWLLogicalAxiom] = - original + ontology .aboxAxioms(Imports.INCLUDED) .collect(Collectors.toList()) .collect { case a: OWLLogicalAxiom => a } @@ -231,9 +238,10 @@ object RSAOntology { case Some(a) => a.approximate(axioms, datafiles) case None => axioms }, - datafiles + datafiles: _* ) } + } /** Wrapper class for an ontology in RSA @@ -241,7 +249,7 @@ object RSAOntology { * @param ontology the input OWL2 ontology. * @param datafiles additinal data (treated as part of the ABox) */ -class RSAOntology(val axioms: Seq[OWLAxiom], val datafiles: File*) { +class RSAOntology(val axioms: List[OWLLogicalAxiom], val datafiles: File*) { /** Simplify conversion between OWLAPI and RDFox concepts */ import implicits.RDFox._ @@ -697,28 +705,28 @@ class RSAOntology(val axioms: Seq[OWLAxiom], val datafiles: File*) { this.self(axiom) | this.cycle(axiom) /** Log normalization/approximation statistics */ - def statistics(level: Logger.Level = Logger.DEBUG): Unit = { - Logger.print( - s"Logical axioms in original input ontology: ${original.getLogicalAxiomCount(true)}", - level - ) - Logger.print( - s"Logical axioms discarded in Horn-ALCHOIQ approximation: ${normalizer.discarded}", - level - ) - Logger.print( - s"Logical axioms shifted in Horn-ALCHOIQ approximation: ${normalizer.shifted}", - level - ) - Logger.print( - s"Logical axioms in Horn-ALCHOIQ ontology: ${ontology - .getLogicalAxiomCount(true)} (${axioms.length}/${axioms.length}/${axioms.length})", - level - ) - Logger.print( - s"Logical axioms discarded in RSA approximation: ${removed.length}", - level - ) - } + // def statistics(level: Logger.Level = Logger.DEBUG): Unit = { + // Logger.print( + // s"Logical axioms in original input ontology: ${original.getLogicalAxiomCount(true)}", + // level + // ) + // Logger.print( + // s"Logical axioms discarded in Horn-ALCHOIQ approximation: ${normalizer.discarded}", + // level + // ) + // Logger.print( + // s"Logical axioms shifted in Horn-ALCHOIQ approximation: ${normalizer.shifted}", + // level + // ) + // Logger.print( + // s"Logical axioms in Horn-ALCHOIQ ontology: ${ontology + // .getLogicalAxiomCount(true)} (${axioms.length}/${axioms.length}/${axioms.length})", + // level + // ) + // Logger.print( + // s"Logical axioms discarded in RSA approximation: ${removed.length}", + // level + // ) + // } } // class RSAOntology diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala index 073d0d9..1b49413 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala @@ -1,7 +1,7 @@ package uk.ac.ox.cs.rsacomb.approximation import java.io.File -import org.semanticweb.owlapi.model.OWLAxiom +import org.semanticweb.owlapi.model.OWLLogicalAxiom /** Ontology approximation technique. */ trait Approximation { @@ -11,6 +11,9 @@ trait Approximation { * @param ontology input ontology * @return a new approximated ontology */ - def approximate(ontology: Seq[OWLAxiom], datafiles: Seq[File]): Seq[OWLAxiom] + def approximate( + ontology: List[OWLLogicalAxiom], + datafiles: List[File] + ): List[OWLLogicalAxiom] } diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala index 8036250..3437bcd 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala @@ -1,5 +1,16 @@ package uk.ac.ox.cs.rsacomb.approximation +import java.io.File + +import org.semanticweb.owlapi.model._ + +import scala.collection.mutable.{Set, Map} +import scalax.collection.Graph +import scalax.collection.GraphPredef._, scalax.collection.GraphEdge._ +import scalax.collection.GraphTraversal._ + +import uk.ac.ox.cs.rsacomb.converter.Normalizer + /** Approximation algorithm that mantains soundness for CQ answering. * * The input OWL 2 ontology is assumed to be normalized and the output @@ -21,11 +32,11 @@ class LowerBound extends Approximation { /** Main entry point for the approximation algorithm */ def approximate( - ontology: Seq[OWLAxiom], - datafiles: Seq[File] - ): Seq[OWLAxiom] = { + ontology: List[OWLLogicalAxiom], + datafiles: List[File] + ): List[OWLLogicalAxiom] = { /* Normalize axioms */ - val axioms1 = axioms flatMap normalizer.normalize + val axioms1 = axioms flatMap normalizer.normalize(_) /* Delete any axiom outside of ALCHOIQ */ val axioms2 = axioms1 filterNot inHornLACHOIQ /* Shift any axiom with disjunction on the rhs */ @@ -95,7 +106,7 @@ class LowerBound extends Approximation { * where nA, nB1, nB2, nB3 are fresh predicates "corresponding" to * the negation of A, B1, B2, B3 respectively. */ - def shift(axiom: OWLLogicalAxiom): Seq[OWLLogicalAxiom] = + def shift(axiom: OWLLogicalAxiom): List[OWLLogicalAxiom] = axiom match { case a: OWLSubClassOfAxiom => { val sub = a.getSubClass.getNNF @@ -135,12 +146,12 @@ class LowerBound extends Approximation { na ) - Seq(r1) ++ r2s ++ r3s + List(r1) ++ r2s ++ r3s } - case _ => Seq(axiom) + case _ => List(axiom) } } - case _ => Seq(axiom) + case _ => List(axiom) } /** Approximate a Horn-ALCHOIQ ontology to RSA @@ -151,7 +162,10 @@ class LowerBound extends Approximation { * @param axioms the set of axioms to approximate. * @return the approximated set of axioms. */ - def toRSA(axioms: Seq[OWLAxiom], datafiles: Seq[File]): Seq[OWLAxiom] = { + def toRSA( + axioms: List[OWLLogicalAxiom], + datafiles: List[File] + ): List[OWLLogicalAxiom] = { /* Compute the dependency graph for the ontology */ val (graph, nodemap) = RSAUtil.dependencyGraph(axioms, datafiles) @@ -192,11 +206,12 @@ class LowerBound extends Approximation { val toDelete = color.iterator.collect { case (resource: IRI, ToDelete) => nodemap(resource.getIRI) - }.toSeq + }.toList /* Remove axioms from approximated ontology */ axioms diff toDelete } + // val edges1 = Seq('A ~> 'B, 'B ~> 'C, 'C ~> 'D, 'D ~> 'H, 'H ~> // 'G, 'G ~> 'F, 'E ~> 'A, 'E ~> 'F, 'B ~> 'E, 'F ~> 'G, 'B ~> 'F, // 'C ~> 'G, 'D ~> 'C, 'H ~> 'D) -- cgit v1.2.3 From 6b4226c41ec2a2439fb44a312ccaff01769e8212 Mon Sep 17 00:00:00 2001 From: Federico Igne Date: Tue, 1 Jun 2021 11:20:39 +0100 Subject: Fix minor compilation errors --- src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala | 3 +- .../scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala | 130 +++++++++++---------- .../ox/cs/rsacomb/approximation/lowerbound.scala | 31 ++++- .../uk/ac/ox/cs/rsacomb/converter/Normalizer.scala | 25 ++-- 4 files changed, 106 insertions(+), 83 deletions(-) (limited to 'src') diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala index a807f75..f2d1a5d 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala @@ -106,12 +106,11 @@ object RSAComb extends App { /* Command-line options */ val config = RSAConfig.parse(args.toList) - val ontology = RSAOntology( + val rsa = RSAOntology( config('ontology).get[File], config('data).get[List[File]], None ) - val rsa = ontology.toRSA() if (config contains 'query) { val query = diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala index 4d0f13d..247b5d5 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala @@ -80,7 +80,7 @@ object RSAUtil { * construction of the dependency graph is computed regardless. The * input axioms are assumed to be normalized. */ - private def dependencyGraph( + def dependencyGraph( axioms: List[OWLLogicalAxiom], datafiles: List[File] ): (Graph[Resource, DiEdge], Map[String, OWLAxiom]) = { @@ -166,18 +166,23 @@ object RSAOntology { import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ + /** Manager instance to interface with OWLAPI */ + val manager = OWLManager.createOWLOntologyManager() + val factory = manager.getOWLDataFactory() + /** Name of the RDFox data store used for CQ answering */ private val DataStore = "answer_computation" - /** Simple fresh variable generator */ + /** Simple fresh variable/class generator */ private var counter = -1; def genFreshVariable(): Variable = { counter += 1 Variable.create(f"I$counter%05d") } - - /** Manager instance to interface with OWLAPI */ - val manager = OWLManager.createOWLOntologyManager() + def getFreshOWLClass(): OWLClass = { + counter += 1 + factory.getOWLClass(s"X$counter") + } def apply( axioms: List[OWLLogicalAxiom], @@ -254,13 +259,16 @@ class RSAOntology(val axioms: List[OWLLogicalAxiom], val datafiles: File*) { /** Simplify conversion between OWLAPI and RDFox concepts */ import implicits.RDFox._ import uk.ac.ox.cs.rsacomb.implicits.RSAAxiom._ + + /** Simplify conversion between Java and Scala collections */ import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ /** Set of axioms removed during the approximation to RSA */ private var removed: Seq[OWLAxiom] = Seq.empty /** Normalized Horn-ALCHOIQ ontology */ - val ontology = RSAOntology.manager.createOntology(axioms.asJava) + val ontology = + RSAOntology.manager.createOntology((axioms: List[OWLAxiom]).asJava) /** OWLAPI internal reasoner instantiated over the approximated ontology */ private val reasoner = @@ -340,61 +348,61 @@ class RSAOntology(val axioms: List[OWLLogicalAxiom], val datafiles: File*) { * @param graph the graph used to compute the axioms to remove. * @param nodemap map from graph nodes to ontology axioms. */ - def toRSA(): RSAOntology = Logger.timed( - { - - /* Compute the dependency graph for the ontology */ - val (graph, nodemap) = this.dependencyGraph() - - /* Define node colors for the graph visit */ - sealed trait NodeColor - case object Unvisited extends NodeColor - case object Visited extends NodeColor - case object ToDelete extends NodeColor - - /* Keep track of node colors during graph visit */ - var color = Map.from[Resource, NodeColor]( - graph.nodes.toOuter.map(k => (k, Unvisited)) - ) - - for { - component <- graph.componentTraverser().map(_ to Graph) - edge <- component - .outerEdgeTraverser(component.nodes.head) - .withKind(BreadthFirst) - } yield { - val source = edge._1 - val target = edge._2 - color(source) match { - case Unvisited | Visited => { - color(target) match { - case Unvisited => - color(source) = Visited; - color(target) = Visited - case Visited => - color(source) = ToDelete - case ToDelete => - color(source) = Visited - } - } - case ToDelete => - } - } - - val toDelete = color.iterator.collect { case (resource: IRI, ToDelete) => - nodemap(resource.getIRI) - }.toSeq - - /* Remove axioms from approximated ontology */ - ontology.removeAxioms(toDelete: _*) - this.removed = toDelete - - /* Return RSA ontology */ - RSAOntology(ontology, datafiles: _*) - }, - "Horn-ALCHOIQ to RSA approximation:", - Logger.DEBUG - ) + // def toRSA(): RSAOntology = Logger.timed( + // { + + // /* Compute the dependency graph for the ontology */ + // val (graph, nodemap) = this.dependencyGraph() + + // /* Define node colors for the graph visit */ + // sealed trait NodeColor + // case object Unvisited extends NodeColor + // case object Visited extends NodeColor + // case object ToDelete extends NodeColor + + // /* Keep track of node colors during graph visit */ + // var color = Map.from[Resource, NodeColor]( + // graph.nodes.toOuter.map(k => (k, Unvisited)) + // ) + + // for { + // component <- graph.componentTraverser().map(_ to Graph) + // edge <- component + // .outerEdgeTraverser(component.nodes.head) + // .withKind(BreadthFirst) + // } yield { + // val source = edge._1 + // val target = edge._2 + // color(source) match { + // case Unvisited | Visited => { + // color(target) match { + // case Unvisited => + // color(source) = Visited; + // color(target) = Visited + // case Visited => + // color(source) = ToDelete + // case ToDelete => + // color(source) = Visited + // } + // } + // case ToDelete => + // } + // } + + // val toDelete = color.iterator.collect { case (resource: IRI, ToDelete) => + // nodemap(resource.getIRI) + // }.toSeq + + // /* Remove axioms from approximated ontology */ + // ontology.removeAxioms(toDelete: _*) + // this.removed = toDelete + + // /* Return RSA ontology */ + // RSAOntology(ontology, datafiles: _*) + // }, + // "Horn-ALCHOIQ to RSA approximation:", + // Logger.DEBUG + // ) // val edges1 = Seq('A ~> 'B, 'B ~> 'C, 'C ~> 'D, 'D ~> 'H, 'H ~> // 'G, 'G ~> 'F, 'E ~> 'A, 'E ~> 'F, 'B ~> 'E, 'F ~> 'G, 'B ~> 'F, // 'C ~> 'G, 'D ~> 'C, 'H ~> 'D) diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala index 3437bcd..8a86d19 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala @@ -2,13 +2,18 @@ package uk.ac.ox.cs.rsacomb.approximation import java.io.File -import org.semanticweb.owlapi.model._ +import org.semanticweb.owlapi.apibinding.OWLManager +import org.semanticweb.owlapi.model.{IRI => _, _} + +import tech.oxfordsemantic.jrdfox.logic.expression.{Resource, IRI} import scala.collection.mutable.{Set, Map} import scalax.collection.Graph import scalax.collection.GraphPredef._, scalax.collection.GraphEdge._ import scalax.collection.GraphTraversal._ +import uk.ac.ox.cs.rsacomb.RSAOntology +import uk.ac.ox.cs.rsacomb.RSAUtil import uk.ac.ox.cs.rsacomb.converter.Normalizer /** Approximation algorithm that mantains soundness for CQ answering. @@ -28,6 +33,16 @@ import uk.ac.ox.cs.rsacomb.converter.Normalizer */ class LowerBound extends Approximation { + /** Simplify conversion between Java and Scala collections */ + import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ + + /** Simplify conversion between OWLAPI and RDFox concepts */ + import uk.ac.ox.cs.rsacomb.implicits.RDFox._ + + /** Manager instance to interface with OWLAPI */ + val manager = OWLManager.createOWLOntologyManager() + val factory = manager.getOWLDataFactory() + val normalizer = new Normalizer() /** Main entry point for the approximation algorithm */ @@ -36,14 +51,14 @@ class LowerBound extends Approximation { datafiles: List[File] ): List[OWLLogicalAxiom] = { /* Normalize axioms */ - val axioms1 = axioms flatMap normalizer.normalize(_) + val axioms1 = ontology flatMap normalizer.normalize /* Delete any axiom outside of ALCHOIQ */ - val axioms2 = axioms1 filterNot inHornLACHOIQ + val axioms2 = axioms1 filterNot inALCHOIQ /* Shift any axiom with disjunction on the rhs */ val axioms3 = for { a1 <- axioms1 a2 <- shift(a1) - a3 <- normalize(a2) + a3 <- normalizer.normalize(a2) } yield a3 /* Approximate to RSA */ toRSA(axioms3, datafiles) @@ -113,8 +128,12 @@ class LowerBound extends Approximation { val sup = a.getSuperClass.getNNF sup match { case sup: OWLObjectUnionOf => { - val body = sub.asConjunctSet.map((atom) => (atom, freshOWLClass())) - val head = sup.asDisjunctSet.map((atom) => (atom, freshOWLClass())) + val body = sub.asConjunctSet.map((atom) => + (atom, RSAOntology.getFreshOWLClass()) + ) + val head = sup.asDisjunctSet.map((atom) => + (atom, RSAOntology.getFreshOWLClass()) + ) val r1 = factory.getOWLSubClassOfAxiom( diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/converter/Normalizer.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/converter/Normalizer.scala index 5329f26..4b298f4 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/converter/Normalizer.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/converter/Normalizer.scala @@ -4,6 +4,7 @@ import org.semanticweb.owlapi.apibinding.OWLManager import org.semanticweb.owlapi.model._ import uk.ac.ox.cs.rsacomb.util.Logger +import uk.ac.ox.cs.rsacomb.RSAOntology object Normalizer { @@ -25,12 +26,6 @@ class Normalizer() { /** Simplify conversion between Java and Scala collections */ import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ - private var counter = -1 - def freshOWLClass(): OWLClass = { - counter += 1 - factory.getOWLClass(s"X$counter") - } - /** Statistics */ var discarded = 0 var shifted = 0 @@ -58,7 +53,7 @@ class Normalizer() { * C c D -> { C c X, X c D } */ case _ if !sub.isOWLClass && !sup.isOWLClass => { - val cls = freshOWLClass() + val cls = RSAOntology.getFreshOWLClass() Seq( factory.getOWLSubClassOfAxiom(sub, cls), factory.getOWLSubClassOfAxiom(cls, sup) @@ -79,7 +74,7 @@ class Normalizer() { if (conj.isOWLClass) (acc1 :+ conj, acc2) else { - val cls = freshOWLClass() + val cls = RSAOntology.getFreshOWLClass() ( acc1 :+ cls, acc2 :+ factory.getOWLSubClassOfAxiom(conj, cls) @@ -138,7 +133,7 @@ class Normalizer() { */ case (sub: OWLObjectSomeValuesFrom, _) if !sub.getFiller.isOWLClass => { - val cls = freshOWLClass() + val cls = RSAOntology.getFreshOWLClass() Seq( factory.getOWLSubClassOfAxiom(sub.getFiller, cls), factory.getOWLSubClassOfAxiom( @@ -153,7 +148,7 @@ class Normalizer() { */ case (_, sup: OWLObjectSomeValuesFrom) if !sup.getFiller.isOWLClass => { - val cls = freshOWLClass() + val cls = RSAOntology.getFreshOWLClass() Seq( factory.getOWLSubClassOfAxiom(cls, sup.getFiller), factory.getOWLSubClassOfAxiom( @@ -298,7 +293,7 @@ class Normalizer() { ) case (_, sup: OWLObjectMaxCardinality) if sup.getCardinality == 1 && !sup.getFiller.isOWLClass => { - val cls = freshOWLClass() + val cls = RSAOntology.getFreshOWLClass() Seq( factory.getOWLSubClassOfAxiom(cls, sup.getFiller), factory.getOWLSubClassOfAxiom( @@ -488,7 +483,7 @@ class Normalizer() { * C(a) -> { X(a), X c C } */ case a: OWLClassAssertionAxiom if !a.getClassExpression.isOWLClass => { - val cls = freshOWLClass() + val cls = RSAOntology.getFreshOWLClass() Seq( factory.getOWLClassAssertionAxiom(cls, a.getIndividual), factory.getOWLSubClassOfAxiom(cls, a.getClassExpression) @@ -532,8 +527,10 @@ class Normalizer() { sub: OWLClassExpression, sup: OWLObjectUnionOf ): Seq[OWLLogicalAxiom] = { - val body = sub.asConjunctSet.map((atom) => (atom, freshOWLClass())) - val head = sup.asDisjunctSet.map((atom) => (atom, freshOWLClass())) + val body = + sub.asConjunctSet.map((atom) => (atom, RSAOntology.getFreshOWLClass())) + val head = + sup.asDisjunctSet.map((atom) => (atom, RSAOntology.getFreshOWLClass())) /* Update statistics */ shifted += 1 -- cgit v1.2.3 From f0d1bfe564853a63128ad139520c9838778a7b61 Mon Sep 17 00:00:00 2001 From: Federico Igne Date: Tue, 20 Jul 2021 09:59:52 +0100 Subject: Add generic Ontology wrapper for common tasks --- .../uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala | 90 ++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala (limited to 'src') diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala new file mode 100644 index 0000000..d7e57dd --- /dev/null +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala @@ -0,0 +1,90 @@ +/* + * Copyright 2020, 2021 KRR Oxford + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.ac.ox.cs.rsacomb.ontology + +import java.io.File + +import org.semanticweb.owlapi.apibinding.OWLManager +import org.semanticweb.owlapi.model.{OWLOntology, OWLAxiom, OWLLogicalAxiom} +import org.semanticweb.owlapi.model.{OWLObjectPropertyExpression} +import org.semanticweb.owlapi.reasoner.structural.StructuralReasonerFactory + +object Ontology { + + /** Manager instance to interface with OWLAPI */ + val manager = OWLManager.createOWLOntologyManager() + //val factory = manager.getOWLDataFactory() + +} + +/** A wrapper for + */ +class Ontology(val axioms: List[OWLLogicalAxiom], val datafiles: List[File]) { + + /** Extend OWLAxiom functionalities */ + import uk.ac.ox.cs.rsacomb.implicits.RSAAxiom._ + + /** OWLOntology based on input axioms */ + private val ontology: OWLOntology = + Ontology.manager.createOntology((axioms: List[OWLAxiom]).asJava) + + /** OWLAPI internal reasoner for ontology */ + private val reasoner = + (new StructuralReasonerFactory()).createReasoner(ontology) + + /** Unsafe roles in the ontology + * + * Unsafety conditions are the following: + * + * 1) For all roles r1 appearing in an axiom of type T5, r1 is unsafe + * if there exists a role r2 (different from top) appearing in an + * axiom of type T3 and r1 is a subproperty of the inverse of r2. + * + * 2) For all roles p1 appearing in an axiom of type T5, p1 is unsafe + * if there exists a role p2 appearing in an axiom of type T4 and + * p1 is a subproperty of either p2 or the inverse of p2. + */ + lazy val unsafe: List[OWLObjectPropertyExpression] = { + + /* Checking for unsafety condition (1) */ + val unsafe1 = for { + axiom <- axioms + if axiom.isT5 + role1 <- axiom.objectPropertyExpressionsInSignature + roleSuper = role1 +: reasoner.superObjectProperties(role1) + axiom <- axioms + if axiom.isT3 && !axiom.isT3top + role2 <- axiom.objectPropertyExpressionsInSignature + if roleSuper contains role2.getInverseProperty + } yield role1 + + /* Checking for unsafety condition (2) */ + val unsafe2 = for { + axiom <- axioms + if axiom.isT5 + role1 <- axiom.objectPropertyExpressionsInSignature + roleSuper = role1 +: reasoner.superObjectProperties(role1) + axiom <- axioms + if axiom.isT4 + role2 <- axiom.objectPropertyExpressionsInSignature + if roleSuper.contains(role2) || + roleSuper.contains(role2.getInverseProperty) + } yield role1 + + unsafe1 ++ unsafe2 + } +} -- cgit v1.2.3 From 53646646f924887768688794aee46874ed194673 Mon Sep 17 00:00:00 2001 From: Federico Igne Date: Tue, 20 Jul 2021 15:09:20 +0100 Subject: Generalize dependency graph generation The code to generate the dependency graph has been moved in the companion object of the generic OWL 2 ontology wrapper Ontology. This signals that we could potentially build a dependency graph for any ontology (and not only RSA ontology). Moreover, a dependency graph can be build for an Ontology object or an arbitrary TBox and Abox. --- .../scala/uk/ac/ox/cs/rsacomb/CanonicalModel.scala | 2 +- src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala | 10 ++ .../scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala | 111 ++-------------- .../ox/cs/rsacomb/approximation/lowerbound.scala | 4 +- .../uk/ac/ox/cs/rsacomb/converter/Normalizer.scala | 17 +-- .../ox/cs/rsacomb/converter/RDFoxConverter.scala | 13 +- .../uk/ac/ox/cs/rsacomb/implicits/RSAAtom.scala | 3 +- .../uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala | 141 ++++++++++++++++++++- 8 files changed, 182 insertions(+), 119 deletions(-) (limited to 'src') diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/CanonicalModel.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/CanonicalModel.scala index 6621f59..af6c463 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/CanonicalModel.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/CanonicalModel.scala @@ -76,7 +76,7 @@ class CanonicalModel(val ontology: RSAOntology) { val (facts, rules): (List[TupleTableAtom], List[Rule]) = { // Compute rules from ontology axioms val (facts, rules) = { - val term = RSAOntology.genFreshVariable() + val term = RSAUtil.genFreshVariable() val unsafe = ontology.unsafeRoles ontology.axioms .map(a => diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala index b749401..82da9df 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala @@ -103,6 +103,16 @@ object RSAConfig { /** Main entry point to the program */ object RSAComb extends App { + /* + * TODO: Aiming for this workflow: + * + * implicit val manager = new Manager(...) + * val original = manager.importFromFile("ontology.owl") + * val axioms = original.getAxioms.filter(isLogicalAxiom).normalize(normalizer) + * val ontology = new Ontology(axioms, data) + * val rsa = ontology.toRSA(approximator) + */ + /* Command-line options */ val config = RSAConfig.parse(args.toList) diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala index bbbbcf3..9902fcd 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala @@ -71,108 +71,10 @@ object RSAUtil { // manager.createOntology(axioms.asJava) // } - /** Compute the RSA dependency graph for a set of axioms - * - * @return a tuple containing the dependency graph and a map between - * the newly introduced constants and the corresponding input axioms. - * - * @note no check on the ontology language is performed since the - * construction of the dependency graph is computed regardless. The - * input axioms are assumed to be normalized. - */ - def dependencyGraph( - axioms: List[OWLLogicalAxiom], - datafiles: List[File] - ): (Graph[Resource, DiEdge], Map[String, OWLAxiom]) = { - val unsafe = RSAOntology(axioms, datafiles).unsafeRoles - var nodemap = Map.empty[String, OWLAxiom] - - object RSAConverter extends RDFoxConverter { - - override def convert( - expr: OWLClassExpression, - term: Term, - unsafe: List[OWLObjectPropertyExpression], - skolem: SkolemStrategy, - suffix: RSASuffix - ): Shards = - (expr, skolem) match { - - case (e: OWLObjectSomeValuesFrom, c: Constant) => { - nodemap.update(c.iri.getIRI, c.axiom) - val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) - if (unsafe contains e.getProperty) - (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) - else - (RSA.PE(term, c.iri) :: res, ext) - } - - case (e: OWLDataSomeValuesFrom, c: Constant) => { - nodemap.update(c.iri.getIRI, c.axiom) - val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) - if (unsafe contains e.getProperty) - (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) - else - (RSA.PE(term, c.iri) :: res, ext) - } - - case _ => super.convert(expr, term, unsafe, skolem, suffix) - } - } - - /* Ontology convertion into LP rules */ - val term = RSAOntology.genFreshVariable() - val result = axioms.map(a => - RSAConverter.convert(a, term, unsafe, new Constant(a), Empty) - ) - - val datalog = result.unzip - val facts = datalog._1.flatten - var rules = datalog._2.flatten - - /* Open connection with RDFox */ - val (server, data) = RDFoxUtil.openConnection("rsa_dependency_graph") - - /* Add additional built-in rules */ - val varX = Variable.create("X") - val varY = Variable.create("Y") - rules = Rule.create( - RSA.E(varX, varY), - RSA.PE(varX, varY), - RSA.U(varX), - RSA.U(varY) - ) :: rules - /* Load facts and rules from ontology */ - RDFoxUtil.addFacts(data, facts) - RDFoxUtil.addRules(data, rules) - /* Load data files */ - RDFoxUtil.addData(data, datafiles: _*) - - /* Build the graph */ - val query = "SELECT ?X ?Y WHERE { ?X rsa:E ?Y }" - val answers = RDFoxUtil.submitQuery(data, query, RSA.Prefixes).get - var edges: Seq[DiEdge[Resource]] = - answers.collect { case (_, Seq(n1, n2)) => n1 ~> n2 } - val graph = Graph(edges: _*) - - /* Close connection to RDFox */ - RDFoxUtil.closeConnection(server, data) - - (graph, nodemap) - } -} - -object RSAOntology { - - import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ - /** Manager instance to interface with OWLAPI */ val manager = OWLManager.createOWLOntologyManager() val factory = manager.getOWLDataFactory() - /** Name of the RDFox data store used for CQ answering */ - private val DataStore = "answer_computation" - /** Simple fresh variable/class generator */ private var counter = -1; def genFreshVariable(): Variable = { @@ -184,6 +86,19 @@ object RSAOntology { factory.getOWLClass(s"X$counter") } +} + +object RSAOntology { + + import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ + + /** Manager instance to interface with OWLAPI */ + val manager = OWLManager.createOWLOntologyManager() + val factory = manager.getOWLDataFactory() + + /** Name of the RDFox data store used for CQ answering */ + private val DataStore = "answer_computation" + def apply( axioms: List[OWLLogicalAxiom], datafiles: List[File] diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala index 8a86d19..2750cca 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala @@ -129,10 +129,10 @@ class LowerBound extends Approximation { sup match { case sup: OWLObjectUnionOf => { val body = sub.asConjunctSet.map((atom) => - (atom, RSAOntology.getFreshOWLClass()) + (atom, RSAUtil.getFreshOWLClass()) ) val head = sup.asDisjunctSet.map((atom) => - (atom, RSAOntology.getFreshOWLClass()) + (atom, RSAUtil.getFreshOWLClass()) ) val r1 = diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/converter/Normalizer.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/converter/Normalizer.scala index 4b298f4..80dd222 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/converter/Normalizer.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/converter/Normalizer.scala @@ -5,6 +5,7 @@ import org.semanticweb.owlapi.model._ import uk.ac.ox.cs.rsacomb.util.Logger import uk.ac.ox.cs.rsacomb.RSAOntology +import uk.ac.ox.cs.rsacomb.RSAUtil object Normalizer { @@ -53,7 +54,7 @@ class Normalizer() { * C c D -> { C c X, X c D } */ case _ if !sub.isOWLClass && !sup.isOWLClass => { - val cls = RSAOntology.getFreshOWLClass() + val cls = RSAUtil.getFreshOWLClass() Seq( factory.getOWLSubClassOfAxiom(sub, cls), factory.getOWLSubClassOfAxiom(cls, sup) @@ -74,7 +75,7 @@ class Normalizer() { if (conj.isOWLClass) (acc1 :+ conj, acc2) else { - val cls = RSAOntology.getFreshOWLClass() + val cls = RSAUtil.getFreshOWLClass() ( acc1 :+ cls, acc2 :+ factory.getOWLSubClassOfAxiom(conj, cls) @@ -133,7 +134,7 @@ class Normalizer() { */ case (sub: OWLObjectSomeValuesFrom, _) if !sub.getFiller.isOWLClass => { - val cls = RSAOntology.getFreshOWLClass() + val cls = RSAUtil.getFreshOWLClass() Seq( factory.getOWLSubClassOfAxiom(sub.getFiller, cls), factory.getOWLSubClassOfAxiom( @@ -148,7 +149,7 @@ class Normalizer() { */ case (_, sup: OWLObjectSomeValuesFrom) if !sup.getFiller.isOWLClass => { - val cls = RSAOntology.getFreshOWLClass() + val cls = RSAUtil.getFreshOWLClass() Seq( factory.getOWLSubClassOfAxiom(cls, sup.getFiller), factory.getOWLSubClassOfAxiom( @@ -293,7 +294,7 @@ class Normalizer() { ) case (_, sup: OWLObjectMaxCardinality) if sup.getCardinality == 1 && !sup.getFiller.isOWLClass => { - val cls = RSAOntology.getFreshOWLClass() + val cls = RSAUtil.getFreshOWLClass() Seq( factory.getOWLSubClassOfAxiom(cls, sup.getFiller), factory.getOWLSubClassOfAxiom( @@ -483,7 +484,7 @@ class Normalizer() { * C(a) -> { X(a), X c C } */ case a: OWLClassAssertionAxiom if !a.getClassExpression.isOWLClass => { - val cls = RSAOntology.getFreshOWLClass() + val cls = RSAUtil.getFreshOWLClass() Seq( factory.getOWLClassAssertionAxiom(cls, a.getIndividual), factory.getOWLSubClassOfAxiom(cls, a.getClassExpression) @@ -528,9 +529,9 @@ class Normalizer() { sup: OWLObjectUnionOf ): Seq[OWLLogicalAxiom] = { val body = - sub.asConjunctSet.map((atom) => (atom, RSAOntology.getFreshOWLClass())) + sub.asConjunctSet.map((atom) => (atom, RSAUtil.getFreshOWLClass())) val head = - sup.asDisjunctSet.map((atom) => (atom, RSAOntology.getFreshOWLClass())) + sup.asDisjunctSet.map((atom) => (atom, RSAUtil.getFreshOWLClass())) /* Update statistics */ shifted += 1 diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/converter/RDFoxConverter.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/converter/RDFoxConverter.scala index 3fac46f..1adf325 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/converter/RDFoxConverter.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/converter/RDFoxConverter.scala @@ -11,6 +11,7 @@ import tech.oxfordsemantic.jrdfox.logic.datalog.{ TupleTableAtom } import tech.oxfordsemantic.jrdfox.logic.expression.{Term, IRI, FunctionCall} +import uk.ac.ox.cs.rsacomb.RSAUtil import uk.ac.ox.cs.rsacomb.RSAOntology import uk.ac.ox.cs.rsacomb.suffix.{Empty, Inverse, RSASuffix} import uk.ac.ox.cs.rsacomb.util.{RSA, RDFoxUtil} @@ -143,14 +144,14 @@ trait RDFoxConverter { } case a: OWLSubObjectPropertyOfAxiom => { - val term1 = RSAOntology.genFreshVariable() + val term1 = RSAUtil.genFreshVariable() val body = convert(a.getSubProperty, term, term1, suffix) val head = convert(a.getSuperProperty, term, term1, suffix) ResultR(List(Rule.create(head, body))) } case a: OWLSubDataPropertyOfAxiom => { - val term1 = RSAOntology.genFreshVariable() + val term1 = RSAUtil.genFreshVariable() val body = convert(a.getSubProperty, term, term1, suffix) val head = convert(a.getSuperProperty, term, term1, suffix) ResultR(List(Rule.create(head, body))) @@ -160,7 +161,7 @@ trait RDFoxConverter { convert(a.asOWLSubClassOfAxiom, term, unsafe, skolem, suffix) case a: OWLObjectPropertyRangeAxiom => { - val term1 = RSAOntology.genFreshVariable() + val term1 = RSAUtil.genFreshVariable() val (res, ext) = convert(a.getRange, term, unsafe, skolem, suffix) val prop = convert(a.getProperty, term1, term, suffix) ResultR(List(Rule.create(res, prop :: ext))) @@ -327,7 +328,7 @@ trait RDFoxConverter { case e: OWLObjectSomeValuesFrom => { val cls = e.getFiller() val role = e.getProperty() - val varX = RSAOntology.genFreshVariable + val varX = RSAUtil.genFreshVariable val (bind, term1) = skolem match { case NoSkolem => (None, varX) case c: Constant => (None, c.iri) @@ -354,7 +355,7 @@ trait RDFoxConverter { // Computes the result of rule skolemization. Depending on the used // technique it might involve the introduction of additional atoms, // and/or fresh constants and variables. - val varX = RSAOntology.genFreshVariable + val varX = RSAUtil.genFreshVariable val (bind, term1) = skolem match { case NoSkolem => (None, varX) case c: Constant => (None, c.iri) @@ -379,7 +380,7 @@ trait RDFoxConverter { s"Class expression '$e' has cardinality restriction != 1." ) val vars @ (y :: z :: _) = - Seq(RSAOntology.genFreshVariable(), RSAOntology.genFreshVariable()) + Seq(RSAUtil.genFreshVariable(), RSAUtil.genFreshVariable()) val cls = e.getFiller val role = e.getProperty val (res, ext) = vars.map(convert(cls, _, unsafe, skolem, suffix)).unzip diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/implicits/RSAAtom.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/implicits/RSAAtom.scala index 9b04f0e..a0d1b5d 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/implicits/RSAAtom.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/implicits/RSAAtom.scala @@ -9,6 +9,7 @@ import tech.oxfordsemantic.jrdfox.logic.datalog.{ } import tech.oxfordsemantic.jrdfox.logic.expression.{IRI} +import uk.ac.ox.cs.rsacomb.RSAUtil import uk.ac.ox.cs.rsacomb.RSAOntology import uk.ac.ox.cs.rsacomb.suffix.{RSASuffix, Nth} import uk.ac.ox.cs.rsacomb.util.RDFoxUtil @@ -78,7 +79,7 @@ object RSAAtom { if (isRDF) { (None, List(atom)) } else { - val varS = RSAOntology.genFreshVariable() + val varS = RSAUtil.genFreshVariable() val skolem = RDFoxUtil.skolem(name, (args :+ varS): _*) val atom = TupleTableAtom.rdf(varS, IRI.RDF_TYPE, name) val atoms = args.zipWithIndex diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala index d7e57dd..7856b3a 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala @@ -18,27 +18,158 @@ package uk.ac.ox.cs.rsacomb.ontology import java.io.File +import scala.collection.mutable.Map +import scala.collection.JavaConverters._ +import scalax.collection.Graph +import scalax.collection.GraphPredef._, scalax.collection.GraphEdge._ + import org.semanticweb.owlapi.apibinding.OWLManager import org.semanticweb.owlapi.model.{OWLOntology, OWLAxiom, OWLLogicalAxiom} import org.semanticweb.owlapi.model.{OWLObjectPropertyExpression} import org.semanticweb.owlapi.reasoner.structural.StructuralReasonerFactory +import tech.oxfordsemantic.jrdfox.logic.expression.Resource object Ontology { - /** Manager instance to interface with OWLAPI */ + /** Type wrapper representing a dependency graph for the ontology. + * + * The graph is returned along with a map associating each node (IRI + * string of the resource), with the corresponding axiom in the + * original TBox. + */ + type DependencyGraph = (Graph[Resource, DiEdge], Map[String, OWLAxiom]) + + /** Manager instance to interface with OWLAPI + * + * TODO: turn this into an implicit class parameter. + */ val manager = OWLManager.createOWLOntologyManager() //val factory = manager.getOWLDataFactory() + /** Compute the RSA dependency graph for a set of axioms + * + * @param axioms set of input axioms (TBox) to build the dependency + * graph. + * @param datafiles data (ABox) to build the dependency graph. + * @param unsafe list of unsafe roles in the TBox. + * + * @return a tuple containing the dependency graph and a map between + * the newly introduced constants and the corresponding input axioms. + * + * @note no check on the ontology language is performed since the + * construction of the dependency graph is computed regardless. The + * input axioms are assumed to be normalized. + */ + def dependencyGraph( + axioms: List[OWLLogicalAxiom], + datafiles: List[File], + unsafe: List[OWLObjectPropertyExpression] + ): DependencyGraph = { + + import org.semanticweb.owlapi.model.{ + OWLClassExpression, + OWLObjectSomeValuesFrom, + OWLDataSomeValuesFrom + } + import tech.oxfordsemantic.jrdfox.logic.datalog.Rule + import tech.oxfordsemantic.jrdfox.logic.expression.{Term, Variable} + import uk.ac.ox.cs.rsacomb.suffix._ + import uk.ac.ox.cs.rsacomb.converter._ + import uk.ac.ox.cs.rsacomb.util.{RDFoxUtil, RSA} + import uk.ac.ox.cs.rsacomb.RSAUtil + + var nodemap = Map.empty[String, OWLAxiom] + + object RSAConverter extends RDFoxConverter { + + override def convert( + expr: OWLClassExpression, + term: Term, + unsafe: List[OWLObjectPropertyExpression], + skolem: SkolemStrategy, + suffix: RSASuffix + ): Shards = + (expr, skolem) match { + + case (e: OWLObjectSomeValuesFrom, c: Constant) => { + nodemap.update(c.iri.getIRI, c.axiom) + val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) + if (unsafe contains e.getProperty) + (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) + else + (RSA.PE(term, c.iri) :: res, ext) + } + + case (e: OWLDataSomeValuesFrom, c: Constant) => { + nodemap.update(c.iri.getIRI, c.axiom) + val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) + if (unsafe contains e.getProperty) + (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) + else + (RSA.PE(term, c.iri) :: res, ext) + } + + case _ => super.convert(expr, term, unsafe, skolem, suffix) + } + } + + /* Ontology convertion into LP rules */ + val term = RSAUtil.genFreshVariable() + val result = axioms.map(a => + RSAConverter.convert(a, term, unsafe, new Constant(a), Empty) + ) + + val datalog = result.unzip + val facts = datalog._1.flatten + var rules = datalog._2.flatten + + /* Open connection with RDFox */ + val (server, data) = RDFoxUtil.openConnection("rsa_dependency_graph") + + /* Add additional built-in rules */ + val varX = Variable.create("X") + val varY = Variable.create("Y") + rules = Rule.create( + RSA.E(varX, varY), + RSA.PE(varX, varY), + RSA.U(varX), + RSA.U(varY) + ) :: rules + /* Load facts and rules from ontology */ + RDFoxUtil.addFacts(data, facts) + RDFoxUtil.addRules(data, rules) + /* Load data files */ + RDFoxUtil.addData(data, datafiles: _*) + + /* Build the graph */ + val query = "SELECT ?X ?Y WHERE { ?X rsa:E ?Y }" + val answers = RDFoxUtil.submitQuery(data, query, RSA.Prefixes).get + var edges: Seq[DiEdge[Resource]] = + answers.collect { case (_, Seq(n1, n2)) => n1 ~> n2 } + val graph = Graph(edges: _*) + + /* Close connection to RDFox */ + RDFoxUtil.closeConnection(server, data) + + (graph, nodemap) + } } -/** A wrapper for +/** A wrapper for a generic OWL2 ontology */ class Ontology(val axioms: List[OWLLogicalAxiom], val datafiles: List[File]) { /** Extend OWLAxiom functionalities */ import uk.ac.ox.cs.rsacomb.implicits.RSAAxiom._ - /** OWLOntology based on input axioms */ + /** Simplify conversion between Java and Scala collections */ + import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ + + /** OWLOntology based on input axioms + * + * This is mainly used to instantiate a new reasoner to be used in + * the computation of unsafe roles. + */ private val ontology: OWLOntology = Ontology.manager.createOntology((axioms: List[OWLAxiom]).asJava) @@ -87,4 +218,8 @@ class Ontology(val axioms: List[OWLLogicalAxiom], val datafiles: List[File]) { unsafe1 ++ unsafe2 } + + lazy val dependencyGraph: Ontology.DependencyGraph = + Ontology.dependencyGraph(axioms, datafiles, this.unsafe) + } -- cgit v1.2.3 From 18ddefc7c9e9cfaca027a054495325737ae6b9e6 Mon Sep 17 00:00:00 2001 From: Federico Igne Date: Thu, 22 Jul 2021 08:29:11 +0100 Subject: Move some generic commands from RSAOntology to Ontology --- .../scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala | 445 ++++++++++----------- .../cs/rsacomb/approximation/approximation.scala | 4 +- .../uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala | 58 ++- 3 files changed, 248 insertions(+), 259 deletions(-) (limited to 'src') diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala index 9902fcd..630d2a0 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala @@ -63,6 +63,7 @@ import uk.ac.ox.cs.rsacomb.suffix._ import uk.ac.ox.cs.rsacomb.sparql._ import uk.ac.ox.cs.rsacomb.util.{RDFoxUtil, RSA} import uk.ac.ox.cs.rsacomb.util.Logger +import uk.ac.ox.cs.rsacomb.ontology.Ontology object RSAUtil { @@ -94,82 +95,94 @@ object RSAOntology { /** Manager instance to interface with OWLAPI */ val manager = OWLManager.createOWLOntologyManager() - val factory = manager.getOWLDataFactory() /** Name of the RDFox data store used for CQ answering */ private val DataStore = "answer_computation" + /** Filtering program for a given query + * + * @param query the query to derive the filtering program + * @return the filtering program for the given query + */ + def filteringProgram(query: ConjunctiveQuery): FilteringProgram = + Logger.timed( + FilteringProgram(FilterType.REVISED)(query), + "Generating filtering program", + Logger.DEBUG + ) + def apply( axioms: List[OWLLogicalAxiom], datafiles: List[File] - ): RSAOntology = new RSAOntology(axioms, datafiles: _*) - - def apply( - ontofile: File, - datafiles: List[File], - approx: Option[Approximation] - ): RSAOntology = { - val ontology = manager.loadOntologyFromOntologyDocument(ontofile) - RSAOntology(ontology, datafiles, approx) - } + ): RSAOntology = new RSAOntology(axioms, datafiles) + + // def apply( + // ontofile: File, + // datafiles: List[File], + // approx: Option[Approximation] + // ): RSAOntology = { + // val ontology = manager.loadOntologyFromOntologyDocument(ontofile) + // RSAOntology(ontology, datafiles, approx) + // } - def apply( - ontology: OWLOntology, - datafiles: List[File], - approx: Option[Approximation] - ): RSAOntology = { - val normalizer = new Normalizer() - - /** TBox axioms */ - var tbox: List[OWLLogicalAxiom] = - ontology - .tboxAxioms(Imports.INCLUDED) - .collect(Collectors.toList()) - .collect { case a: OWLLogicalAxiom => a } - .flatMap(normalizer.normalize) - - /** RBox axioms */ - var rbox: List[OWLLogicalAxiom] = - ontology - .rboxAxioms(Imports.INCLUDED) - .collect(Collectors.toList()) - .collect { case a: OWLLogicalAxiom => a } - .flatMap(normalizer.normalize) - - /** ABox axioms - * - * @note this represents only the set of assertions contained in the - * ontology file. Data files specified in `datafiles` are directly - * imported in RDFox due to performance issues when trying to import - * large data files via OWLAPI. - */ - var abox: List[OWLLogicalAxiom] = - ontology - .aboxAxioms(Imports.INCLUDED) - .collect(Collectors.toList()) - .collect { case a: OWLLogicalAxiom => a } - .flatMap(normalizer.normalize) - - /** Collection of logical axioms in the input ontology */ - var axioms: List[OWLLogicalAxiom] = abox ::: tbox ::: rbox - - new RSAOntology( - approx match { - case Some(a) => a.approximate(axioms, datafiles) - case None => axioms - }, - datafiles: _* - ) - } + // def apply( + // ontology: OWLOntology, + // datafiles: List[File], + // approx: Option[Approximation] + // ): RSAOntology = { + // val normalizer = new Normalizer() + + // /** TBox axioms */ + // var tbox: List[OWLLogicalAxiom] = + // ontology + // .tboxAxioms(Imports.INCLUDED) + // .collect(Collectors.toList()) + // .collect { case a: OWLLogicalAxiom => a } + // .flatMap(normalizer.normalize) + + // /** RBox axioms */ + // var rbox: List[OWLLogicalAxiom] = + // ontology + // .rboxAxioms(Imports.INCLUDED) + // .collect(Collectors.toList()) + // .collect { case a: OWLLogicalAxiom => a } + // .flatMap(normalizer.normalize) + + // /** ABox axioms + // * + // * @note this represents only the set of assertions contained in the + // * ontology file. Data files specified in `datafiles` are directly + // * imported in RDFox due to performance issues when trying to import + // * large data files via OWLAPI. + // */ + // var abox: List[OWLLogicalAxiom] = + // ontology + // .aboxAxioms(Imports.INCLUDED) + // .collect(Collectors.toList()) + // .collect { case a: OWLLogicalAxiom => a } + // .flatMap(normalizer.normalize) + + // /** Collection of logical axioms in the input ontology */ + // var axioms: List[OWLLogicalAxiom] = abox ::: tbox ::: rbox + + // new RSAOntology( + // approx match { + // case Some(a) => a.approximate(axioms, datafiles) + // case None => axioms + // }, + // datafiles: _* + // ) + // } } -/** Wrapper class for an ontology in RSA +/** A wrapper for an RSA ontology * * @param ontology the input OWL2 ontology. * @param datafiles additinal data (treated as part of the ABox) */ -class RSAOntology(val axioms: List[OWLLogicalAxiom], val datafiles: File*) { +class RSAOntology(axioms: List[OWLLogicalAxiom], datafiles: List[File]) + extends Ontology(axioms, datafiles) { /** Simplify conversion between OWLAPI and RDFox concepts */ import implicits.RDFox._ @@ -179,34 +192,26 @@ class RSAOntology(val axioms: List[OWLLogicalAxiom], val datafiles: File*) { import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ /** Set of axioms removed during the approximation to RSA */ - private var removed: Seq[OWLAxiom] = Seq.empty - - /** Normalized Horn-ALCHOIQ ontology */ - val ontology = - RSAOntology.manager.createOntology((axioms: List[OWLAxiom]).asJava) - - /** OWLAPI internal reasoner instantiated over the approximated ontology */ - private val reasoner = - (new StructuralReasonerFactory()).createReasoner(ontology) + //private var removed: Seq[OWLAxiom] = Seq.empty /** Retrieve individuals/literals in the ontology */ - val individuals: List[IRI] = + private val individuals: List[IRI] = ontology .getIndividualsInSignature() .asScala .map(_.getIRI) .map(implicits.RDFox.owlapiToRdfoxIri) .toList - val literals: List[Literal] = + private val literals: List[Literal] = axioms .collect { case a: OWLDataPropertyAssertionAxiom => a } .map(_.getObject) .map(implicits.RDFox.owlapiToRdfoxLiteral) /** Retrieve concepts/roles in the ontology */ - val concepts: List[OWLClass] = + private val concepts: List[OWLClass] = ontology.getClassesInSignature().asScala.toList - val roles: List[OWLObjectPropertyExpression] = + private val roles: List[OWLObjectPropertyExpression] = axioms .flatMap(_.objectPropertyExpressionsInSignature) .distinct @@ -223,36 +228,36 @@ class RSAOntology(val axioms: List[OWLLogicalAxiom], val datafiles: File*) { * if there exists a role p2 appearing in an axiom of type T4 and * p1 is a subproperty of either p2 or the inverse of p2. */ - val unsafeRoles: List[OWLObjectPropertyExpression] = { - - /* Checking for unsafety condition (1) */ - val unsafe1 = for { - axiom <- axioms - if axiom.isT5 - role1 <- axiom.objectPropertyExpressionsInSignature - roleSuper = role1 +: reasoner.superObjectProperties(role1) - roleSuperInv = roleSuper.map(_.getInverseProperty) - axiom <- axioms - if axiom.isT3 && !axiom.isT3top - role2 <- axiom.objectPropertyExpressionsInSignature - if roleSuperInv contains role2 - } yield role1 - - /* Checking for unsafety condition (2) */ - val unsafe2 = for { - axiom <- axioms - if axiom.isT5 - role1 <- axiom.objectPropertyExpressionsInSignature - roleSuper = role1 +: reasoner.superObjectProperties(role1) - roleSuperInv = roleSuper.map(_.getInverseProperty) - axiom <- axioms - if axiom.isT4 - role2 <- axiom.objectPropertyExpressionsInSignature - if roleSuper.contains(role2) || roleSuperInv.contains(role2) - } yield role1 - - unsafe1 ++ unsafe2 - } + // val unsafeRoles: List[OWLObjectPropertyExpression] = { + + // /* Checking for unsafety condition (1) */ + // val unsafe1 = for { + // axiom <- axioms + // if axiom.isT5 + // role1 <- axiom.objectPropertyExpressionsInSignature + // roleSuper = role1 +: reasoner.superObjectProperties(role1) + // roleSuperInv = roleSuper.map(_.getInverseProperty) + // axiom <- axioms + // if axiom.isT3 && !axiom.isT3top + // role2 <- axiom.objectPropertyExpressionsInSignature + // if roleSuperInv contains role2 + // } yield role1 + + // /* Checking for unsafety condition (2) */ + // val unsafe2 = for { + // axiom <- axioms + // if axiom.isT5 + // role1 <- axiom.objectPropertyExpressionsInSignature + // roleSuper = role1 +: reasoner.superObjectProperties(role1) + // roleSuperInv = roleSuper.map(_.getInverseProperty) + // axiom <- axioms + // if axiom.isT4 + // role2 <- axiom.objectPropertyExpressionsInSignature + // if roleSuper.contains(role2) || roleSuperInv.contains(role2) + // } yield role1 + + // unsafe1 ++ unsafe2 + // } /** Approximate a Horn-ALCHOIQ ontology to RSA * @@ -396,31 +401,27 @@ class RSAOntology(val axioms: List[OWLLogicalAxiom], val datafiles: File*) { ) } + /** Canonical model of the ontology */ lazy val canonicalModel = Logger.timed( new CanonicalModel(this), "Generating canonical model program", Logger.DEBUG ) - def filteringProgram(query: ConjunctiveQuery): FilteringProgram = - Logger.timed( - FilteringProgram(FilterType.REVISED)(query), - "Generating filtering program", - Logger.DEBUG - ) - + /** Computes all roles conflicting with a given role + * + * @param role a role (object property expression). + * @return a set of roles conflicting with `role`. + */ def confl( role: OWLObjectPropertyExpression ): Set[OWLObjectPropertyExpression] = { - - val invSuperRoles = reasoner + reasoner .superObjectProperties(role) .collect(Collectors.toSet()) .asScala .addOne(role) .map(_.getInverseProperty) - - invSuperRoles .flatMap(x => reasoner .subObjectProperties(x) @@ -432,6 +433,77 @@ class RSAOntology(val axioms: List[OWLLogicalAxiom], val datafiles: File*) { .filterNot(_.getInverseProperty.isOWLTopObjectProperty()) } + /** Selfloop detection for a given axiom + * + * @param axiom an axiom of type [[OWLSubClassOfAxiom]] + * @return unfold set for the axiom + */ + def self(axiom: OWLSubClassOfAxiom): Set[Term] = { + val role = axiom.objectPropertyExpressionsInSignature(0) + if (this.confl(role).contains(role)) { + Set(RSA("v0_" ++ axiom.hashed), RSA("v1_" ++ axiom.hashed)) + } else { + Set() + } + } + + /** Cycle detection for a give axiom + * + * @param axiom an axiom of type [[OWLSubClassOfAxiom]] + * @return unfold set for the axiom + * + * @todo we can actually use `toTriple` from `RSAAxiom` to get the + * classes and the role for a given axiom + */ + def cycle(axiom: OWLSubClassOfAxiom): Set[Term] = { + val classes = + axiom.classesInSignature.collect(Collectors.toList()).asScala + val classA = classes(0) + val roleR = axiom + .objectPropertyExpressionsInSignature(0) + .asInstanceOf[OWLObjectProperty] + val classB = classes(1) + cycle_aux(classA, roleR, classB) + } + + /** Auxiliary function for [[RSAOntology.cycle]] */ + private def cycle_aux( + classA: OWLClass, + roleR: OWLObjectProperty, + classB: OWLClass + ): Set[Term] = { + val conflR = this.confl(roleR) + // TODO: technically we just need the TBox here + val terms = for { + axiom1 <- axioms + if axiom1.isT5 + // We expect only one role coming out of a T5 axiom + roleS <- axiom1.objectPropertyExpressionsInSignature + // Triples ordering is among triples involving safe roles. + if !unsafe.contains(roleS) + if conflR.contains(roleS) + tripleARB = RSAAxiom.hashed(classA, roleR, classB) + tripleDSC = axiom1.hashed + individual = + if (tripleARB > tripleDSC) { + RSA("v1_" ++ tripleDSC) + } else { + // Note that this is also the case for + // `tripleARB == tripleDSC` + RSA("v0_" ++ tripleDSC) + } + } yield individual + terms to Set + } + + /** Returns unfold set for self-loop and cycle for the input axiom + * + * @param axiom an axiom of type [[OWLSubClassOfAxiom]] + * @return unfold set for the axiom + */ + def unfold(axiom: OWLSubClassOfAxiom): Set[Term] = + this.self(axiom) | this.cycle(axiom) + /** Returns the answers to a query * * @param query query to execute @@ -439,10 +511,9 @@ class RSAOntology(val axioms: List[OWLLogicalAxiom], val datafiles: File*) { */ def ask(query: ConjunctiveQuery): ConjunctiveQueryAnswers = Logger.timed( { - import implicits.JavaCollections._ val (server, data) = RDFoxUtil.openConnection(RSAOntology.DataStore) val canon = this.canonicalModel - val filter = this.filteringProgram(query) + val filter = RSAOntology.filteringProgram(query) /* Upload data from data file */ RDFoxUtil.addData(data, datafiles: _*) @@ -460,12 +531,15 @@ class RSAOntology(val axioms: List[OWLLogicalAxiom], val datafiles: File*) { new java.util.HashMap[String, String] ) + /* Add canonical model */ Logger print s"Canonical model rules: ${canon.rules.length}" RDFoxUtil.addRules(data, canon.rules) Logger print s"Canonical model facts: ${canon.facts.length}" RDFoxUtil.addFacts(data, canon.facts) + RDFoxUtil printStatisticsFor data + //{ // import java.io.{PrintStream, FileOutputStream, File} // val rules1 = new FileOutputStream(new File("rules1-lubm200.dlog")) @@ -475,16 +549,13 @@ class RSAOntology(val axioms: List[OWLLogicalAxiom], val datafiles: File*) { // rules2.print(filter.rules.mkString("\n")) //} - //canon.facts.foreach(println) - //filter.rules.foreach(println) - - RDFoxUtil printStatisticsFor data - + /* Add filtering program */ Logger print s"Filtering program rules: ${filter.rules.length}" RDFoxUtil.addRules(data, filter.rules) RDFoxUtil printStatisticsFor data + /* Gather answers to the query */ val answers = { val ans = filter.answerQuery RDFoxUtil @@ -492,7 +563,9 @@ class RSAOntology(val axioms: List[OWLLogicalAxiom], val datafiles: File*) { .map(new ConjunctiveQueryAnswers(query.bcq, query.variables, _)) .get } + RDFoxUtil.closeConnection(server, data) + answers }, "Answers computation", @@ -503,14 +576,15 @@ class RSAOntology(val axioms: List[OWLLogicalAxiom], val datafiles: File*) { * * @note This method does not add any facts or rules to the data * store. It is most useful after the execution of a query using - * [[uk.ac.ox.cs.rsacomb.RSAOntology.ask RSAOntology.ask]]. - * @note This method has been introduced mostly for debugging purposes. + * [[RSAOntology.ask]]. * * @param query query to be executed against the environment * @param prefixes additional prefixes for the query. It defaults to * an empty set. * @param opts additional options to RDFox. * @return a collection of answers to the input query. + * + * @note This method has been introduced mostly for debugging purposes. */ def queryDataStore( query: String, @@ -532,122 +606,11 @@ class RSAOntology(val axioms: List[OWLLogicalAxiom], val datafiles: File*) { * [[uk.ac.ox.cs.rsacomb.RSAOntology.ask RSAOntology.ask]] * for the corresponding query has been called. */ - def askUnfiltered( - cq: ConjunctiveQuery - ): Option[Seq[(Long, Seq[Resource])]] = { - val query = RDFoxUtil.buildDescriptionQuery("QM", cq.variables.length) - queryDataStore(query, RSA.Prefixes) - } - - def self(axiom: OWLSubClassOfAxiom): Set[Term] = { - // Assuming just one role in the signature of a T5 axiom - val role = axiom.objectPropertyExpressionsInSignature(0) - if (this.confl(role).contains(role)) { - Set( - RSA("v0_" ++ axiom.hashed), - RSA("v1_" ++ axiom.hashed) - ) - } else { - Set() - } - } - - def cycle(axiom: OWLSubClassOfAxiom): Set[Term] = { - // TODO: we can actually use `toTriple` from `RSAAxiom` - val classes = - axiom.classesInSignature.collect(Collectors.toList()).asScala - val classA = classes(0) - val roleR = axiom - .objectPropertyExpressionsInSignature(0) - .asInstanceOf[OWLObjectProperty] - val classB = classes(1) - cycle_aux1(classA, roleR, classB) - } - - def cycle_aux0( - classA: OWLClass, - roleR: OWLObjectProperty, - classB: OWLClass - ): Set[Term] = { - val conflR = this.confl(roleR) - val classes = ontology - .classesInSignature(Imports.INCLUDED) - .collect(Collectors.toSet()) - .asScala - for { - classD <- classes - roleS <- conflR - classC <- classes - // Keeping this check for now - if !unsafeRoles.contains(roleS) - tripleARB = RSAAxiom.hashed(classA, roleR, classB) - tripleDSC = RSAAxiom.hashed(classD, roleS, classC) - individual = - if (tripleARB > tripleDSC) { - RSA("v1_" ++ tripleDSC) - } else { - // Note that this is also the case for - // `tripleARB == tripleDSC` - RSA("v0_" ++ tripleDSC) - } - } yield individual - } - - def cycle_aux1( - classA: OWLClass, - roleR: OWLObjectProperty, - classB: OWLClass - ): Set[Term] = { - val conflR = this.confl(roleR) - // We just need the TBox to find - val terms = for { - axiom1 <- axioms - if axiom1.isT5 - // We expect only one role coming out of a T5 axiom - roleS <- axiom1.objectPropertyExpressionsInSignature - // Triples ordering is among triples involving safe roles. - if !unsafeRoles.contains(roleS) - if conflR.contains(roleS) - tripleARB = RSAAxiom.hashed(classA, roleR, classB) - tripleDSC = axiom1.hashed - individual = - if (tripleARB > tripleDSC) { - RSA("v1_" ++ tripleDSC) - } else { - // Note that this is also the case for - // `tripleARB == tripleDSC` - RSA("v0_" ++ tripleDSC) - } - } yield individual - terms to Set - } - - def unfold(axiom: OWLSubClassOfAxiom): Set[Term] = - this.self(axiom) | this.cycle(axiom) - - /** Log normalization/approximation statistics */ - // def statistics(level: Logger.Level = Logger.DEBUG): Unit = { - // Logger.print( - // s"Logical axioms in original input ontology: ${original.getLogicalAxiomCount(true)}", - // level - // ) - // Logger.print( - // s"Logical axioms discarded in Horn-ALCHOIQ approximation: ${normalizer.discarded}", - // level - // ) - // Logger.print( - // s"Logical axioms shifted in Horn-ALCHOIQ approximation: ${normalizer.shifted}", - // level - // ) - // Logger.print( - // s"Logical axioms in Horn-ALCHOIQ ontology: ${ontology - // .getLogicalAxiomCount(true)} (${axioms.length}/${axioms.length}/${axioms.length})", - // level - // ) - // Logger.print( - // s"Logical axioms discarded in RSA approximation: ${removed.length}", - // level - // ) + // def askUnfiltered( + // cq: ConjunctiveQuery + // ): Option[Seq[(Long, Seq[Resource])]] = { + // val query = RDFoxUtil.buildDescriptionQuery("QM", cq.variables.length) + // queryDataStore(query, RSA.Prefixes) // } -} // class RSAOntology +} diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala index 1b49413..db2118f 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala @@ -8,8 +8,8 @@ trait Approximation { /** Approximate an ontology. * - * @param ontology input ontology - * @return a new approximated ontology + * @param ontology input ontology as a list of axioms + * @return the approximated ontology */ def approximate( ontology: List[OWLLogicalAxiom], diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala index 7856b3a..4a23ec7 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala @@ -27,7 +27,15 @@ import org.semanticweb.owlapi.apibinding.OWLManager import org.semanticweb.owlapi.model.{OWLOntology, OWLAxiom, OWLLogicalAxiom} import org.semanticweb.owlapi.model.{OWLObjectPropertyExpression} import org.semanticweb.owlapi.reasoner.structural.StructuralReasonerFactory -import tech.oxfordsemantic.jrdfox.logic.expression.Resource +import tech.oxfordsemantic.jrdfox.logic.datalog.Rule +import tech.oxfordsemantic.jrdfox.logic.expression.{Resource, Term, Variable} + +import uk.ac.ox.cs.rsacomb.approximation.Approximation +import uk.ac.ox.cs.rsacomb.converter._ +import uk.ac.ox.cs.rsacomb.suffix._ +import uk.ac.ox.cs.rsacomb.util.{RDFoxUtil, RSA} + +import uk.ac.ox.cs.rsacomb.RSAUtil object Ontology { @@ -44,7 +52,6 @@ object Ontology { * TODO: turn this into an implicit class parameter. */ val manager = OWLManager.createOWLOntologyManager() - //val factory = manager.getOWLDataFactory() /** Compute the RSA dependency graph for a set of axioms * @@ -66,22 +73,17 @@ object Ontology { unsafe: List[OWLObjectPropertyExpression] ): DependencyGraph = { - import org.semanticweb.owlapi.model.{ - OWLClassExpression, - OWLObjectSomeValuesFrom, - OWLDataSomeValuesFrom - } - import tech.oxfordsemantic.jrdfox.logic.datalog.Rule - import tech.oxfordsemantic.jrdfox.logic.expression.{Term, Variable} - import uk.ac.ox.cs.rsacomb.suffix._ - import uk.ac.ox.cs.rsacomb.converter._ - import uk.ac.ox.cs.rsacomb.util.{RDFoxUtil, RSA} - import uk.ac.ox.cs.rsacomb.RSAUtil - var nodemap = Map.empty[String, OWLAxiom] + /* Create custom converter */ object RSAConverter extends RDFoxConverter { + import org.semanticweb.owlapi.model.{ + OWLClassExpression, + OWLObjectSomeValuesFrom, + OWLDataSomeValuesFrom + } + override def convert( expr: OWLClassExpression, term: Term, @@ -156,6 +158,9 @@ object Ontology { } /** A wrapper for a generic OWL2 ontology + * + * @param axioms list of axioms (roughly) corresponding to the TBox. + * @param datafiles files containing ABox data. */ class Ontology(val axioms: List[OWLLogicalAxiom], val datafiles: List[File]) { @@ -170,11 +175,11 @@ class Ontology(val axioms: List[OWLLogicalAxiom], val datafiles: List[File]) { * This is mainly used to instantiate a new reasoner to be used in * the computation of unsafe roles. */ - private val ontology: OWLOntology = + protected val ontology: OWLOntology = Ontology.manager.createOntology((axioms: List[OWLAxiom]).asJava) /** OWLAPI internal reasoner for ontology */ - private val reasoner = + protected val reasoner = (new StructuralReasonerFactory()).createReasoner(ontology) /** Unsafe roles in the ontology @@ -219,7 +224,28 @@ class Ontology(val axioms: List[OWLLogicalAxiom], val datafiles: List[File]) { unsafe1 ++ unsafe2 } + /** Compute the dependency graph for the ontology */ lazy val dependencyGraph: Ontology.DependencyGraph = Ontology.dependencyGraph(axioms, datafiles, this.unsafe) + /** RSA check */ + lazy val isRSA: Boolean = ??? + + /** Normalize the ontology according to the given normalizer + * + * @param normalizer the normalization technique to be used. + * @return a new normalized [[Ontology]]. + */ + def normalize(normalizer: Normalizer): Ontology = ??? + + /** Approximate the ontology according to the given approximation + * technique. + * + * @param approximation the approximation to be used on the ontology. + * @return a new approximated [[Ontology]]. + */ + def approximate(approximation: Approximation): Ontology = { + val approx = approximation.approximate(axioms, datafiles) + new Ontology(approx, datafiles) + } } -- cgit v1.2.3 From fb0bbb76a53d08dffea74d0ae13c03a122c379bd Mon Sep 17 00:00:00 2001 From: Federico Igne Date: Thu, 22 Jul 2021 09:20:36 +0100 Subject: Make approximation generic over returned type --- .../cs/rsacomb/approximation/approximation.scala | 9 ++-- .../ox/cs/rsacomb/approximation/lowerbound.scala | 54 +++++++++++----------- .../uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala | 8 ++-- 3 files changed, 33 insertions(+), 38 deletions(-) (limited to 'src') diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala index db2118f..344f0fe 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala @@ -3,17 +3,16 @@ package uk.ac.ox.cs.rsacomb.approximation import java.io.File import org.semanticweb.owlapi.model.OWLLogicalAxiom +import uk.ac.ox.cs.rsacomb.ontology.Ontology + /** Ontology approximation technique. */ -trait Approximation { +trait Approximation[T] { /** Approximate an ontology. * * @param ontology input ontology as a list of axioms * @return the approximated ontology */ - def approximate( - ontology: List[OWLLogicalAxiom], - datafiles: List[File] - ): List[OWLLogicalAxiom] + def approximate(ontology: Ontology): T } diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala index 2750cca..07f10a4 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala @@ -15,6 +15,14 @@ import scalax.collection.GraphTraversal._ import uk.ac.ox.cs.rsacomb.RSAOntology import uk.ac.ox.cs.rsacomb.RSAUtil import uk.ac.ox.cs.rsacomb.converter.Normalizer +import uk.ac.ox.cs.rsacomb.ontology.Ontology + +object LowerBound { + + private val manager = OWLManager.createOWLOntologyManager() + private val factory = manager.getOWLDataFactory() + +} /** Approximation algorithm that mantains soundness for CQ answering. * @@ -31,7 +39,7 @@ import uk.ac.ox.cs.rsacomb.converter.Normalizer * * @see [[uk.ac.ox.cs.rsacomb.converter.Normalizer]] */ -class LowerBound extends Approximation { +class LowerBound extends Approximation[RSAOntology] { /** Simplify conversion between Java and Scala collections */ import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ @@ -39,19 +47,12 @@ class LowerBound extends Approximation { /** Simplify conversion between OWLAPI and RDFox concepts */ import uk.ac.ox.cs.rsacomb.implicits.RDFox._ - /** Manager instance to interface with OWLAPI */ - val manager = OWLManager.createOWLOntologyManager() - val factory = manager.getOWLDataFactory() - - val normalizer = new Normalizer() + private val normalizer = new Normalizer() /** Main entry point for the approximation algorithm */ - def approximate( - ontology: List[OWLLogicalAxiom], - datafiles: List[File] - ): List[OWLLogicalAxiom] = { + def approximate(ontology: Ontology): RSAOntology = { /* Normalize axioms */ - val axioms1 = ontology flatMap normalizer.normalize + val axioms1 = ontology.axioms flatMap normalizer.normalize /* Delete any axiom outside of ALCHOIQ */ val axioms2 = axioms1 filterNot inALCHOIQ /* Shift any axiom with disjunction on the rhs */ @@ -61,11 +62,11 @@ class LowerBound extends Approximation { a3 <- normalizer.normalize(a2) } yield a3 /* Approximate to RSA */ - toRSA(axioms3, datafiles) + toRSA(new Ontology(axioms3, ontology.datafiles)) } /** Discards all axioms outside ALCHOIQ */ - def inALCHOIQ(axiom: OWLLogicalAxiom): Boolean = + private def inALCHOIQ(axiom: OWLLogicalAxiom): Boolean = axiom match { case a: OWLSubClassOfAxiom => { val sub = a.getSubClass.getNNF @@ -121,7 +122,7 @@ class LowerBound extends Approximation { * where nA, nB1, nB2, nB3 are fresh predicates "corresponding" to * the negation of A, B1, B2, B3 respectively. */ - def shift(axiom: OWLLogicalAxiom): List[OWLLogicalAxiom] = + private def shift(axiom: OWLLogicalAxiom): List[OWLLogicalAxiom] = axiom match { case a: OWLSubClassOfAxiom => { val sub = a.getSubClass.getNNF @@ -136,19 +137,19 @@ class LowerBound extends Approximation { ) val r1 = - factory.getOWLSubClassOfAxiom( - factory.getOWLObjectIntersectionOf( + LowerBound.factory.getOWLSubClassOfAxiom( + LowerBound.factory.getOWLObjectIntersectionOf( (body.map(_._1) ++ head.map(_._2)): _* ), - factory.getOWLNothing + LowerBound.factory.getOWLNothing ) val r2s = for { (a, na) <- head hs = head.map(_._2).filterNot(_ equals na) - } yield factory.getOWLSubClassOfAxiom( - factory.getOWLObjectIntersectionOf( + } yield LowerBound.factory.getOWLSubClassOfAxiom( + LowerBound.factory.getOWLObjectIntersectionOf( (body.map(_._1) ++ hs): _* ), a @@ -158,8 +159,8 @@ class LowerBound extends Approximation { for { (a, na) <- body bs = body.map(_._1).filterNot(_ equals a) - } yield factory.getOWLSubClassOfAxiom( - factory.getOWLObjectIntersectionOf( + } yield LowerBound.factory.getOWLSubClassOfAxiom( + LowerBound.factory.getOWLObjectIntersectionOf( (bs ++ head.map(_._2)): _* ), na @@ -179,14 +180,11 @@ class LowerBound extends Approximation { * dependency graph from being tree-shaped, and removing them. * * @param axioms the set of axioms to approximate. - * @return the approximated set of axioms. + * @return the approximated RSA ontology */ - def toRSA( - axioms: List[OWLLogicalAxiom], - datafiles: List[File] - ): List[OWLLogicalAxiom] = { + private def toRSA(ontology: Ontology): RSAOntology = { /* Compute the dependency graph for the ontology */ - val (graph, nodemap) = RSAUtil.dependencyGraph(axioms, datafiles) + val (graph, nodemap) = ontology.dependencyGraph /* Define node colors for the graph visit */ sealed trait NodeColor @@ -228,7 +226,7 @@ class LowerBound extends Approximation { }.toList /* Remove axioms from approximated ontology */ - axioms diff toDelete + RSAOntology(ontology.axioms diff toDelete, ontology.datafiles) } // val edges1 = Seq('A ~> 'B, 'B ~> 'C, 'C ~> 'D, 'D ~> 'H, 'H ~> diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala index 4a23ec7..9d947bc 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala @@ -242,10 +242,8 @@ class Ontology(val axioms: List[OWLLogicalAxiom], val datafiles: List[File]) { * technique. * * @param approximation the approximation to be used on the ontology. - * @return a new approximated [[Ontology]]. + * @return the result of the approximation. */ - def approximate(approximation: Approximation): Ontology = { - val approx = approximation.approximate(axioms, datafiles) - new Ontology(approx, datafiles) - } + def approximate[T](approximation: Approximation[T]): T = + approximation.approximate(this) } -- cgit v1.2.3 From cb8572606f8951213bcfe9e6667caa208ad3d189 Mon Sep 17 00:00:00 2001 From: Federico Igne Date: Thu, 22 Jul 2021 09:59:07 +0100 Subject: Review main workflow --- .../scala/uk/ac/ox/cs/rsacomb/CanonicalModel.scala | 2 +- src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala | 26 ++++++------ .../scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala | 4 +- .../uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala | 46 ++++++++++++++++++++++ 4 files changed, 61 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/CanonicalModel.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/CanonicalModel.scala index af6c463..ee808c3 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/CanonicalModel.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/CanonicalModel.scala @@ -77,7 +77,7 @@ class CanonicalModel(val ontology: RSAOntology) { // Compute rules from ontology axioms val (facts, rules) = { val term = RSAUtil.genFreshVariable() - val unsafe = ontology.unsafeRoles + val unsafe = ontology.unsafe ontology.axioms .map(a => CanonicalModelConverter.convert(a, term, unsafe, Constant(a), Empty) diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala index 82da9df..8e5169d 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala @@ -10,6 +10,10 @@ import tech.oxfordsemantic.jrdfox.logic.sparql.statement.SelectQuery import util.{Logger, RDFoxUtil, RSA} import sparql.ConjunctiveQuery +import uk.ac.ox.cs.rsacomb.ontology.Ontology +import uk.ac.ox.cs.rsacomb.converter.Normalizer +import uk.ac.ox.cs.rsacomb.approximation.LowerBound + case class RSAOption[+T](opt: T) { def get[T]: T = opt.asInstanceOf[T] } @@ -103,24 +107,18 @@ object RSAConfig { /** Main entry point to the program */ object RSAComb extends App { - /* - * TODO: Aiming for this workflow: - * - * implicit val manager = new Manager(...) - * val original = manager.importFromFile("ontology.owl") - * val axioms = original.getAxioms.filter(isLogicalAxiom).normalize(normalizer) - * val ontology = new Ontology(axioms, data) - * val rsa = ontology.toRSA(approximator) - */ - /* Command-line options */ val config = RSAConfig.parse(args.toList) - val rsa = RSAOntology( + /* Load original ontology and normalize it */ + val ontology = Ontology( config('ontology).get[File], - config('data).get[List[File]], - None - ) + config('data).get[List[File]] + ).normalize(new Normalizer) + + /* Approximate the ontology to RSA */ + val toRSA = new LowerBound + val rsa = ontology approximate toRSA if (config contains 'query) { val query = diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala index 630d2a0..73c4411 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala @@ -209,9 +209,9 @@ class RSAOntology(axioms: List[OWLLogicalAxiom], datafiles: List[File]) .map(implicits.RDFox.owlapiToRdfoxLiteral) /** Retrieve concepts/roles in the ontology */ - private val concepts: List[OWLClass] = + val concepts: List[OWLClass] = ontology.getClassesInSignature().asScala.toList - private val roles: List[OWLObjectPropertyExpression] = + val roles: List[OWLObjectPropertyExpression] = axioms .flatMap(_.objectPropertyExpressionsInSignature) .distinct diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala index 9d947bc..723bcaa 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala @@ -17,12 +17,14 @@ package uk.ac.ox.cs.rsacomb.ontology import java.io.File +import java.util.stream.Collectors import scala.collection.mutable.Map import scala.collection.JavaConverters._ import scalax.collection.Graph import scalax.collection.GraphPredef._, scalax.collection.GraphEdge._ +import org.semanticweb.owlapi.model.parameters.Imports import org.semanticweb.owlapi.apibinding.OWLManager import org.semanticweb.owlapi.model.{OWLOntology, OWLAxiom, OWLLogicalAxiom} import org.semanticweb.owlapi.model.{OWLObjectPropertyExpression} @@ -39,6 +41,9 @@ import uk.ac.ox.cs.rsacomb.RSAUtil object Ontology { + /** Simplify conversion between Java and Scala collections */ + import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ + /** Type wrapper representing a dependency graph for the ontology. * * The graph is returned along with a map associating each node (IRI @@ -155,6 +160,47 @@ object Ontology { (graph, nodemap) } + + def apply(axioms: List[OWLLogicalAxiom], datafiles: List[File]): Ontology = + new Ontology(axioms, datafiles) + + def apply(ontology: OWLOntology, datafiles: List[File]): Ontology = { + + /** TBox axioms */ + var tbox: List[OWLLogicalAxiom] = + ontology + .tboxAxioms(Imports.INCLUDED) + .collect(Collectors.toList()) + .collect { case a: OWLLogicalAxiom => a } + + /** RBox axioms */ + var rbox: List[OWLLogicalAxiom] = + ontology + .rboxAxioms(Imports.INCLUDED) + .collect(Collectors.toList()) + .collect { case a: OWLLogicalAxiom => a } + + /** ABox axioms + * + * @note this represents only the set of assertions contained in the + * ontology file. Data files specified in `datafiles` are directly + * imported in RDFox due to performance issues when trying to import + * large data files via OWLAPI. + */ + var abox: List[OWLLogicalAxiom] = + ontology + .aboxAxioms(Imports.INCLUDED) + .collect(Collectors.toList()) + .collect { case a: OWLLogicalAxiom => a } + + Ontology(abox ::: tbox ::: rbox, datafiles) + } + + def apply(ontofile: File, datafiles: List[File]): Ontology = { + val ontology = manager.loadOntologyFromOntologyDocument(ontofile) + Ontology(ontology, datafiles) + } + } /** A wrapper for a generic OWL2 ontology -- cgit v1.2.3 From b8b8a69ec1d1c93d5cfcfba4dbd002d5c90dd4c6 Mon Sep 17 00:00:00 2001 From: Federico Igne Date: Thu, 22 Jul 2021 10:29:49 +0100 Subject: Remove normalization step from lowerbound computation --- .../ox/cs/rsacomb/approximation/lowerbound.scala | 27 ++++++++-------------- .../uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala | 6 ++++- 2 files changed, 15 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala index 07f10a4..766ea0e 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala @@ -14,7 +14,6 @@ import scalax.collection.GraphTraversal._ import uk.ac.ox.cs.rsacomb.RSAOntology import uk.ac.ox.cs.rsacomb.RSAUtil -import uk.ac.ox.cs.rsacomb.converter.Normalizer import uk.ac.ox.cs.rsacomb.ontology.Ontology object LowerBound { @@ -47,23 +46,14 @@ class LowerBound extends Approximation[RSAOntology] { /** Simplify conversion between OWLAPI and RDFox concepts */ import uk.ac.ox.cs.rsacomb.implicits.RDFox._ - private val normalizer = new Normalizer() - /** Main entry point for the approximation algorithm */ - def approximate(ontology: Ontology): RSAOntology = { - /* Normalize axioms */ - val axioms1 = ontology.axioms flatMap normalizer.normalize - /* Delete any axiom outside of ALCHOIQ */ - val axioms2 = axioms1 filterNot inALCHOIQ - /* Shift any axiom with disjunction on the rhs */ - val axioms3 = for { - a1 <- axioms1 - a2 <- shift(a1) - a3 <- normalizer.normalize(a2) - } yield a3 - /* Approximate to RSA */ - toRSA(new Ontology(axioms3, ontology.datafiles)) - } + def approximate(ontology: Ontology): RSAOntology = + toRSA( + new Ontology( + ontology.axioms filterNot inALCHOIQ flatMap shift, + ontology.datafiles + ) + ) /** Discards all axioms outside ALCHOIQ */ private def inALCHOIQ(axiom: OWLLogicalAxiom): Boolean = @@ -114,6 +104,7 @@ class LowerBound extends Approximation[RSAOntology] { * * becomes * + * A n nB1 n nB2 n nB3 -> bot . * A n nB1 n nB2 -> B3 . * A n nB1 n nB3 -> B2 . * A n nB2 n nB3 -> B1 . @@ -121,6 +112,8 @@ class LowerBound extends Approximation[RSAOntology] { * * where nA, nB1, nB2, nB3 are fresh predicates "corresponding" to * the negation of A, B1, B2, B3 respectively. + * + * @note this method maintains the normal form of the input axiom. */ private def shift(axiom: OWLLogicalAxiom): List[OWLLogicalAxiom] = axiom match { diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala index 723bcaa..d73704f 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala @@ -282,7 +282,11 @@ class Ontology(val axioms: List[OWLLogicalAxiom], val datafiles: List[File]) { * @param normalizer the normalization technique to be used. * @return a new normalized [[Ontology]]. */ - def normalize(normalizer: Normalizer): Ontology = ??? + def normalize(normalizer: Normalizer): Ontology = + new Ontology( + axioms flatMap normalizer.normalize, + datafiles + ) /** Approximate the ontology according to the given approximation * technique. -- cgit v1.2.3 From 79ca4d0b3fe7df238bede90b96226e72707d05b7 Mon Sep 17 00:00:00 2001 From: Federico Igne Date: Thu, 22 Jul 2021 17:15:42 +0100 Subject: Fix file naming --- .../cs/rsacomb/approximation/Approximation.scala | 18 ++ .../ox/cs/rsacomb/approximation/Lowerbound.scala | 232 ++++++++++++++++ .../cs/rsacomb/approximation/approximation.scala | 18 -- .../ox/cs/rsacomb/approximation/lowerbound.scala | 232 ---------------- .../uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala | 299 --------------------- .../uk/ac/ox/cs/rsacomb/ontology/Ontology.scala | 299 +++++++++++++++++++++ 6 files changed, 549 insertions(+), 549 deletions(-) create mode 100644 src/main/scala/uk/ac/ox/cs/rsacomb/approximation/Approximation.scala create mode 100644 src/main/scala/uk/ac/ox/cs/rsacomb/approximation/Lowerbound.scala delete mode 100644 src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala delete mode 100644 src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala delete mode 100644 src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala create mode 100644 src/main/scala/uk/ac/ox/cs/rsacomb/ontology/Ontology.scala (limited to 'src') diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/Approximation.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/Approximation.scala new file mode 100644 index 0000000..344f0fe --- /dev/null +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/Approximation.scala @@ -0,0 +1,18 @@ +package uk.ac.ox.cs.rsacomb.approximation + +import java.io.File +import org.semanticweb.owlapi.model.OWLLogicalAxiom + +import uk.ac.ox.cs.rsacomb.ontology.Ontology + +/** Ontology approximation technique. */ +trait Approximation[T] { + + /** Approximate an ontology. + * + * @param ontology input ontology as a list of axioms + * @return the approximated ontology + */ + def approximate(ontology: Ontology): T + +} diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/Lowerbound.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/Lowerbound.scala new file mode 100644 index 0000000..3fc4988 --- /dev/null +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/Lowerbound.scala @@ -0,0 +1,232 @@ +package uk.ac.ox.cs.rsacomb.approximation + +import java.io.File + +import org.semanticweb.owlapi.apibinding.OWLManager +import org.semanticweb.owlapi.model.{IRI => _, _} + +import tech.oxfordsemantic.jrdfox.logic.expression.{Resource, IRI} + +import scala.collection.mutable.{Set, Map} +import scalax.collection.Graph +import scalax.collection.GraphPredef._, scalax.collection.GraphEdge._ +import scalax.collection.GraphTraversal._ + +import uk.ac.ox.cs.rsacomb.RSAOntology +import uk.ac.ox.cs.rsacomb.RSAUtil +import uk.ac.ox.cs.rsacomb.ontology.Ontology + +object LowerBound { + + private val manager = OWLManager.createOWLOntologyManager() + private val factory = manager.getOWLDataFactory() + +} + +/** Approximation algorithm that mantains soundness for CQ answering. + * + * The input OWL 2 ontology is assumed to be normalized and the output + * ontology is guaranteed to be in RSA. + * + * The algorithm is performed in three steps: + * 1. the ontology is reduced to ALCHOIQ by discarding any axiom + * that is not in the language; + * 2. the ontology is further reduced to Horn-ALCHOIQ by shifting + * axioms with disjunction on the rhs; + * 3. the ontology is approximated to RSA by manipulating its + * dependency graph. + * + * @see [[uk.ac.ox.cs.rsacomb.converter.Normalizer]] + */ +class LowerBound extends Approximation[RSAOntology] { + + /** Simplify conversion between Java and Scala collections */ + import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ + + /** Simplify conversion between OWLAPI and RDFox concepts */ + import uk.ac.ox.cs.rsacomb.implicits.RDFox._ + + /** Main entry point for the approximation algorithm */ + def approximate(ontology: Ontology): RSAOntology = + toRSA( + new Ontology( + ontology.axioms filterNot inALCHOIQ flatMap shift, + ontology.datafiles + ) + ) + + /** Discards all axioms outside ALCHOIQ */ + private def inALCHOIQ(axiom: OWLLogicalAxiom): Boolean = + axiom match { + case a: OWLSubClassOfAxiom => { + val sub = a.getSubClass.getNNF + val sup = a.getSuperClass.getNNF + (sub, sup) match { + case (sub: OWLObjectAllValuesFrom, _) => false + case (sub: OWLDataAllValuesFrom, _) => false + case (_, sup: OWLDataAllValuesFrom) => false + case (sub: OWLObjectMinCardinality, _) if sub.getCardinality >= 2 => + false + case (sub: OWLDataMinCardinality, _) if sub.getCardinality >= 2 => + false + case (_, sup: OWLObjectMinCardinality) if sup.getCardinality >= 2 => + false + case (_, sup: OWLDataMinCardinality) if sup.getCardinality >= 2 => + false + case (sub: OWLObjectMaxCardinality, _) => false + case (sub: OWLDataMaxCardinality, _) => false + case (_, sup: OWLObjectMaxCardinality) if sup.getCardinality >= 2 => + false + case (_, sup: OWLDataMaxCardinality) if sup.getCardinality >= 1 => + false + case (_, sup: OWLObjectOneOf) if sup.getIndividuals.length > 2 => + false + case (sub: OWLObjectHasSelf, _) => false + case (_, sup: OWLObjectHasSelf) => false + case _ => true + } + } + case a: OWLTransitiveObjectPropertyAxiom => false + case a: OWLReflexiveObjectPropertyAxiom => false + case a: OWLSubPropertyChainOfAxiom => false + case a: OWLAsymmetricObjectPropertyAxiom => false + case a => true + } + + /** Shifting axioms with disjunction on the rhs. + * + * The process of shifting presenves soundness but completenes w.r.t. + * CQ answering is lost. + * + * @example + * + * A -> B1 u B2 u B3 . + * + * becomes + * + * A n nB1 n nB2 n nB3 -> bot . + * A n nB1 n nB2 -> B3 . + * A n nB1 n nB3 -> B2 . + * A n nB2 n nB3 -> B1 . + * nB1 n nB2 n nB3 -> nA . + * + * where nA, nB1, nB2, nB3 are fresh predicates "corresponding" to + * the negation of A, B1, B2, B3 respectively. + * + * @note this method maintains the normal form of the input axiom. + */ + private def shift(axiom: OWLLogicalAxiom): List[OWLLogicalAxiom] = + axiom match { + case a: OWLSubClassOfAxiom => { + val sub = a.getSubClass.getNNF + val sup = a.getSuperClass.getNNF + sup match { + case sup: OWLObjectUnionOf => { + val body = sub.asConjunctSet.map((atom) => + (atom, RSAUtil.getFreshOWLClass()) + ) + val head = sup.asDisjunctSet.map((atom) => + (atom, RSAUtil.getFreshOWLClass()) + ) + + val r1 = + LowerBound.factory.getOWLSubClassOfAxiom( + LowerBound.factory.getOWLObjectIntersectionOf( + (body.map(_._1) ++ head.map(_._2)): _* + ), + LowerBound.factory.getOWLNothing + ) + + val r2s = + for { + (a, na) <- head + hs = head.map(_._2).filterNot(_ equals na) + } yield LowerBound.factory.getOWLSubClassOfAxiom( + LowerBound.factory.getOWLObjectIntersectionOf( + (body.map(_._1) ++ hs): _* + ), + a + ) + + val r3s = + for { + (a, na) <- body + bs = body.map(_._1).filterNot(_ equals a) + } yield LowerBound.factory.getOWLSubClassOfAxiom( + LowerBound.factory.getOWLObjectIntersectionOf( + (bs ++ head.map(_._2)): _* + ), + na + ) + + List(r1) ++ r2s ++ r3s + } + case _ => List(axiom) + } + } + case _ => List(axiom) + } + + /** Approximate a Horn-ALCHOIQ ontology to RSA + * + * This is done by gathering those axioms that prevent the ontology + * dependency graph from being tree-shaped, and removing them. + * + * @param ontology the set of axioms to approximate. + * @return the approximated RSA ontology + */ + private def toRSA(ontology: Ontology): RSAOntology = { + /* Compute the dependency graph for the ontology */ + val (graph, nodemap) = ontology.dependencyGraph + + /* Define node colors for the graph visit */ + sealed trait NodeColor + case object Unvisited extends NodeColor + case object Visited extends NodeColor + case object ToDelete extends NodeColor + + /* Keep track of node colors during graph visit */ + var color = Map.from[Resource, NodeColor]( + graph.nodes.toOuter.map(k => (k, Unvisited)) + ) + + for { + component <- graph.componentTraverser().map(_ to Graph) + edge <- component + .outerEdgeTraverser(component.nodes.head) + .withKind(BreadthFirst) + } yield { + val source = edge._1 + val target = edge._2 + color(source) match { + case Unvisited | Visited => { + color(target) match { + case Unvisited => + color(source) = Visited; + color(target) = Visited + case Visited => + color(source) = ToDelete + case ToDelete => + color(source) = Visited + } + } + case ToDelete => + } + } + + val toDelete = color.collect { case (resource: IRI, ToDelete) => + nodemap(resource.getIRI) + }.toList + + /* Remove axioms from approximated ontology */ + RSAOntology(ontology.axioms diff toDelete, ontology.datafiles) + } + + // val edges1 = Seq('A ~> 'B, 'B ~> 'C, 'C ~> 'D, 'D ~> 'H, 'H ~> + // 'G, 'G ~> 'F, 'E ~> 'A, 'E ~> 'F, 'B ~> 'E, 'F ~> 'G, 'B ~> 'F, + // 'C ~> 'G, 'D ~> 'C, 'H ~> 'D) + // val edges2 = Seq('I ~> 'M, 'I ~> 'L, 'L ~> 'N, 'M ~> 'N) + // val edges3 = Seq('P ~> 'O) + // val graph = Graph.from(edges = edges1 ++ edges2 ++ edges3) + // +} diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala deleted file mode 100644 index 344f0fe..0000000 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala +++ /dev/null @@ -1,18 +0,0 @@ -package uk.ac.ox.cs.rsacomb.approximation - -import java.io.File -import org.semanticweb.owlapi.model.OWLLogicalAxiom - -import uk.ac.ox.cs.rsacomb.ontology.Ontology - -/** Ontology approximation technique. */ -trait Approximation[T] { - - /** Approximate an ontology. - * - * @param ontology input ontology as a list of axioms - * @return the approximated ontology - */ - def approximate(ontology: Ontology): T - -} diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala deleted file mode 100644 index 766ea0e..0000000 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala +++ /dev/null @@ -1,232 +0,0 @@ -package uk.ac.ox.cs.rsacomb.approximation - -import java.io.File - -import org.semanticweb.owlapi.apibinding.OWLManager -import org.semanticweb.owlapi.model.{IRI => _, _} - -import tech.oxfordsemantic.jrdfox.logic.expression.{Resource, IRI} - -import scala.collection.mutable.{Set, Map} -import scalax.collection.Graph -import scalax.collection.GraphPredef._, scalax.collection.GraphEdge._ -import scalax.collection.GraphTraversal._ - -import uk.ac.ox.cs.rsacomb.RSAOntology -import uk.ac.ox.cs.rsacomb.RSAUtil -import uk.ac.ox.cs.rsacomb.ontology.Ontology - -object LowerBound { - - private val manager = OWLManager.createOWLOntologyManager() - private val factory = manager.getOWLDataFactory() - -} - -/** Approximation algorithm that mantains soundness for CQ answering. - * - * The input OWL 2 ontology is assumed to be normalized and the output - * ontology is guaranteed to be in RSA. - * - * The algorithm is performed in three steps: - * 1. the ontology is reduced to ALCHOIQ by discarding any axiom - * that is not in the language; - * 2. the ontology is further reduced to Horn-ALCHOIQ by shifting - * axioms with disjunction on the rhs; - * 3. the ontology is approximated to RSA by manipulating its - * dependency graph. - * - * @see [[uk.ac.ox.cs.rsacomb.converter.Normalizer]] - */ -class LowerBound extends Approximation[RSAOntology] { - - /** Simplify conversion between Java and Scala collections */ - import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ - - /** Simplify conversion between OWLAPI and RDFox concepts */ - import uk.ac.ox.cs.rsacomb.implicits.RDFox._ - - /** Main entry point for the approximation algorithm */ - def approximate(ontology: Ontology): RSAOntology = - toRSA( - new Ontology( - ontology.axioms filterNot inALCHOIQ flatMap shift, - ontology.datafiles - ) - ) - - /** Discards all axioms outside ALCHOIQ */ - private def inALCHOIQ(axiom: OWLLogicalAxiom): Boolean = - axiom match { - case a: OWLSubClassOfAxiom => { - val sub = a.getSubClass.getNNF - val sup = a.getSuperClass.getNNF - (sub, sup) match { - case (sub: OWLObjectAllValuesFrom, _) => false - case (sub: OWLDataAllValuesFrom, _) => false - case (_, sup: OWLDataAllValuesFrom) => false - case (sub: OWLObjectMinCardinality, _) if sub.getCardinality >= 2 => - false - case (sub: OWLDataMinCardinality, _) if sub.getCardinality >= 2 => - false - case (_, sup: OWLObjectMinCardinality) if sup.getCardinality >= 2 => - false - case (_, sup: OWLDataMinCardinality) if sup.getCardinality >= 2 => - false - case (sub: OWLObjectMaxCardinality, _) => false - case (sub: OWLDataMaxCardinality, _) => false - case (_, sup: OWLObjectMaxCardinality) if sup.getCardinality >= 2 => - false - case (_, sup: OWLDataMaxCardinality) if sup.getCardinality >= 1 => - false - case (_, sup: OWLObjectOneOf) if sup.getIndividuals.length > 2 => - false - case (sub: OWLObjectHasSelf, _) => false - case (_, sup: OWLObjectHasSelf) => false - case _ => true - } - } - case a: OWLTransitiveObjectPropertyAxiom => false - case a: OWLReflexiveObjectPropertyAxiom => false - case a: OWLSubPropertyChainOfAxiom => false - case a: OWLAsymmetricObjectPropertyAxiom => false - case a => true - } - - /** Shifting axioms with disjunction on the rhs. - * - * The process of shifting presenves soundness but completenes w.r.t. - * CQ answering is lost. - * - * @example - * - * A -> B1 u B2 u B3 . - * - * becomes - * - * A n nB1 n nB2 n nB3 -> bot . - * A n nB1 n nB2 -> B3 . - * A n nB1 n nB3 -> B2 . - * A n nB2 n nB3 -> B1 . - * nB1 n nB2 n nB3 -> nA . - * - * where nA, nB1, nB2, nB3 are fresh predicates "corresponding" to - * the negation of A, B1, B2, B3 respectively. - * - * @note this method maintains the normal form of the input axiom. - */ - private def shift(axiom: OWLLogicalAxiom): List[OWLLogicalAxiom] = - axiom match { - case a: OWLSubClassOfAxiom => { - val sub = a.getSubClass.getNNF - val sup = a.getSuperClass.getNNF - sup match { - case sup: OWLObjectUnionOf => { - val body = sub.asConjunctSet.map((atom) => - (atom, RSAUtil.getFreshOWLClass()) - ) - val head = sup.asDisjunctSet.map((atom) => - (atom, RSAUtil.getFreshOWLClass()) - ) - - val r1 = - LowerBound.factory.getOWLSubClassOfAxiom( - LowerBound.factory.getOWLObjectIntersectionOf( - (body.map(_._1) ++ head.map(_._2)): _* - ), - LowerBound.factory.getOWLNothing - ) - - val r2s = - for { - (a, na) <- head - hs = head.map(_._2).filterNot(_ equals na) - } yield LowerBound.factory.getOWLSubClassOfAxiom( - LowerBound.factory.getOWLObjectIntersectionOf( - (body.map(_._1) ++ hs): _* - ), - a - ) - - val r3s = - for { - (a, na) <- body - bs = body.map(_._1).filterNot(_ equals a) - } yield LowerBound.factory.getOWLSubClassOfAxiom( - LowerBound.factory.getOWLObjectIntersectionOf( - (bs ++ head.map(_._2)): _* - ), - na - ) - - List(r1) ++ r2s ++ r3s - } - case _ => List(axiom) - } - } - case _ => List(axiom) - } - - /** Approximate a Horn-ALCHOIQ ontology to RSA - * - * This is done by gathering those axioms that prevent the ontology - * dependency graph from being tree-shaped, and removing them. - * - * @param axioms the set of axioms to approximate. - * @return the approximated RSA ontology - */ - private def toRSA(ontology: Ontology): RSAOntology = { - /* Compute the dependency graph for the ontology */ - val (graph, nodemap) = ontology.dependencyGraph - - /* Define node colors for the graph visit */ - sealed trait NodeColor - case object Unvisited extends NodeColor - case object Visited extends NodeColor - case object ToDelete extends NodeColor - - /* Keep track of node colors during graph visit */ - var color = Map.from[Resource, NodeColor]( - graph.nodes.toOuter.map(k => (k, Unvisited)) - ) - - for { - component <- graph.componentTraverser().map(_ to Graph) - edge <- component - .outerEdgeTraverser(component.nodes.head) - .withKind(BreadthFirst) - } yield { - val source = edge._1 - val target = edge._2 - color(source) match { - case Unvisited | Visited => { - color(target) match { - case Unvisited => - color(source) = Visited; - color(target) = Visited - case Visited => - color(source) = ToDelete - case ToDelete => - color(source) = Visited - } - } - case ToDelete => - } - } - - val toDelete = color.iterator.collect { case (resource: IRI, ToDelete) => - nodemap(resource.getIRI) - }.toList - - /* Remove axioms from approximated ontology */ - RSAOntology(ontology.axioms diff toDelete, ontology.datafiles) - } - - // val edges1 = Seq('A ~> 'B, 'B ~> 'C, 'C ~> 'D, 'D ~> 'H, 'H ~> - // 'G, 'G ~> 'F, 'E ~> 'A, 'E ~> 'F, 'B ~> 'E, 'F ~> 'G, 'B ~> 'F, - // 'C ~> 'G, 'D ~> 'C, 'H ~> 'D) - // val edges2 = Seq('I ~> 'M, 'I ~> 'L, 'L ~> 'N, 'M ~> 'N) - // val edges3 = Seq('P ~> 'O) - // val graph = Graph.from(edges = edges1 ++ edges2 ++ edges3) - // -} diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala deleted file mode 100644 index d73704f..0000000 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/ontologies/Ontology.scala +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Copyright 2020, 2021 KRR Oxford - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package uk.ac.ox.cs.rsacomb.ontology - -import java.io.File -import java.util.stream.Collectors - -import scala.collection.mutable.Map -import scala.collection.JavaConverters._ -import scalax.collection.Graph -import scalax.collection.GraphPredef._, scalax.collection.GraphEdge._ - -import org.semanticweb.owlapi.model.parameters.Imports -import org.semanticweb.owlapi.apibinding.OWLManager -import org.semanticweb.owlapi.model.{OWLOntology, OWLAxiom, OWLLogicalAxiom} -import org.semanticweb.owlapi.model.{OWLObjectPropertyExpression} -import org.semanticweb.owlapi.reasoner.structural.StructuralReasonerFactory -import tech.oxfordsemantic.jrdfox.logic.datalog.Rule -import tech.oxfordsemantic.jrdfox.logic.expression.{Resource, Term, Variable} - -import uk.ac.ox.cs.rsacomb.approximation.Approximation -import uk.ac.ox.cs.rsacomb.converter._ -import uk.ac.ox.cs.rsacomb.suffix._ -import uk.ac.ox.cs.rsacomb.util.{RDFoxUtil, RSA} - -import uk.ac.ox.cs.rsacomb.RSAUtil - -object Ontology { - - /** Simplify conversion between Java and Scala collections */ - import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ - - /** Type wrapper representing a dependency graph for the ontology. - * - * The graph is returned along with a map associating each node (IRI - * string of the resource), with the corresponding axiom in the - * original TBox. - */ - type DependencyGraph = (Graph[Resource, DiEdge], Map[String, OWLAxiom]) - - /** Manager instance to interface with OWLAPI - * - * TODO: turn this into an implicit class parameter. - */ - val manager = OWLManager.createOWLOntologyManager() - - /** Compute the RSA dependency graph for a set of axioms - * - * @param axioms set of input axioms (TBox) to build the dependency - * graph. - * @param datafiles data (ABox) to build the dependency graph. - * @param unsafe list of unsafe roles in the TBox. - * - * @return a tuple containing the dependency graph and a map between - * the newly introduced constants and the corresponding input axioms. - * - * @note no check on the ontology language is performed since the - * construction of the dependency graph is computed regardless. The - * input axioms are assumed to be normalized. - */ - def dependencyGraph( - axioms: List[OWLLogicalAxiom], - datafiles: List[File], - unsafe: List[OWLObjectPropertyExpression] - ): DependencyGraph = { - - var nodemap = Map.empty[String, OWLAxiom] - - /* Create custom converter */ - object RSAConverter extends RDFoxConverter { - - import org.semanticweb.owlapi.model.{ - OWLClassExpression, - OWLObjectSomeValuesFrom, - OWLDataSomeValuesFrom - } - - override def convert( - expr: OWLClassExpression, - term: Term, - unsafe: List[OWLObjectPropertyExpression], - skolem: SkolemStrategy, - suffix: RSASuffix - ): Shards = - (expr, skolem) match { - - case (e: OWLObjectSomeValuesFrom, c: Constant) => { - nodemap.update(c.iri.getIRI, c.axiom) - val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) - if (unsafe contains e.getProperty) - (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) - else - (RSA.PE(term, c.iri) :: res, ext) - } - - case (e: OWLDataSomeValuesFrom, c: Constant) => { - nodemap.update(c.iri.getIRI, c.axiom) - val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) - if (unsafe contains e.getProperty) - (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) - else - (RSA.PE(term, c.iri) :: res, ext) - } - - case _ => super.convert(expr, term, unsafe, skolem, suffix) - } - } - - /* Ontology convertion into LP rules */ - val term = RSAUtil.genFreshVariable() - val result = axioms.map(a => - RSAConverter.convert(a, term, unsafe, new Constant(a), Empty) - ) - - val datalog = result.unzip - val facts = datalog._1.flatten - var rules = datalog._2.flatten - - /* Open connection with RDFox */ - val (server, data) = RDFoxUtil.openConnection("rsa_dependency_graph") - - /* Add additional built-in rules */ - val varX = Variable.create("X") - val varY = Variable.create("Y") - rules = Rule.create( - RSA.E(varX, varY), - RSA.PE(varX, varY), - RSA.U(varX), - RSA.U(varY) - ) :: rules - /* Load facts and rules from ontology */ - RDFoxUtil.addFacts(data, facts) - RDFoxUtil.addRules(data, rules) - /* Load data files */ - RDFoxUtil.addData(data, datafiles: _*) - - /* Build the graph */ - val query = "SELECT ?X ?Y WHERE { ?X rsa:E ?Y }" - val answers = RDFoxUtil.submitQuery(data, query, RSA.Prefixes).get - var edges: Seq[DiEdge[Resource]] = - answers.collect { case (_, Seq(n1, n2)) => n1 ~> n2 } - val graph = Graph(edges: _*) - - /* Close connection to RDFox */ - RDFoxUtil.closeConnection(server, data) - - (graph, nodemap) - } - - def apply(axioms: List[OWLLogicalAxiom], datafiles: List[File]): Ontology = - new Ontology(axioms, datafiles) - - def apply(ontology: OWLOntology, datafiles: List[File]): Ontology = { - - /** TBox axioms */ - var tbox: List[OWLLogicalAxiom] = - ontology - .tboxAxioms(Imports.INCLUDED) - .collect(Collectors.toList()) - .collect { case a: OWLLogicalAxiom => a } - - /** RBox axioms */ - var rbox: List[OWLLogicalAxiom] = - ontology - .rboxAxioms(Imports.INCLUDED) - .collect(Collectors.toList()) - .collect { case a: OWLLogicalAxiom => a } - - /** ABox axioms - * - * @note this represents only the set of assertions contained in the - * ontology file. Data files specified in `datafiles` are directly - * imported in RDFox due to performance issues when trying to import - * large data files via OWLAPI. - */ - var abox: List[OWLLogicalAxiom] = - ontology - .aboxAxioms(Imports.INCLUDED) - .collect(Collectors.toList()) - .collect { case a: OWLLogicalAxiom => a } - - Ontology(abox ::: tbox ::: rbox, datafiles) - } - - def apply(ontofile: File, datafiles: List[File]): Ontology = { - val ontology = manager.loadOntologyFromOntologyDocument(ontofile) - Ontology(ontology, datafiles) - } - -} - -/** A wrapper for a generic OWL2 ontology - * - * @param axioms list of axioms (roughly) corresponding to the TBox. - * @param datafiles files containing ABox data. - */ -class Ontology(val axioms: List[OWLLogicalAxiom], val datafiles: List[File]) { - - /** Extend OWLAxiom functionalities */ - import uk.ac.ox.cs.rsacomb.implicits.RSAAxiom._ - - /** Simplify conversion between Java and Scala collections */ - import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ - - /** OWLOntology based on input axioms - * - * This is mainly used to instantiate a new reasoner to be used in - * the computation of unsafe roles. - */ - protected val ontology: OWLOntology = - Ontology.manager.createOntology((axioms: List[OWLAxiom]).asJava) - - /** OWLAPI internal reasoner for ontology */ - protected val reasoner = - (new StructuralReasonerFactory()).createReasoner(ontology) - - /** Unsafe roles in the ontology - * - * Unsafety conditions are the following: - * - * 1) For all roles r1 appearing in an axiom of type T5, r1 is unsafe - * if there exists a role r2 (different from top) appearing in an - * axiom of type T3 and r1 is a subproperty of the inverse of r2. - * - * 2) For all roles p1 appearing in an axiom of type T5, p1 is unsafe - * if there exists a role p2 appearing in an axiom of type T4 and - * p1 is a subproperty of either p2 or the inverse of p2. - */ - lazy val unsafe: List[OWLObjectPropertyExpression] = { - - /* Checking for unsafety condition (1) */ - val unsafe1 = for { - axiom <- axioms - if axiom.isT5 - role1 <- axiom.objectPropertyExpressionsInSignature - roleSuper = role1 +: reasoner.superObjectProperties(role1) - axiom <- axioms - if axiom.isT3 && !axiom.isT3top - role2 <- axiom.objectPropertyExpressionsInSignature - if roleSuper contains role2.getInverseProperty - } yield role1 - - /* Checking for unsafety condition (2) */ - val unsafe2 = for { - axiom <- axioms - if axiom.isT5 - role1 <- axiom.objectPropertyExpressionsInSignature - roleSuper = role1 +: reasoner.superObjectProperties(role1) - axiom <- axioms - if axiom.isT4 - role2 <- axiom.objectPropertyExpressionsInSignature - if roleSuper.contains(role2) || - roleSuper.contains(role2.getInverseProperty) - } yield role1 - - unsafe1 ++ unsafe2 - } - - /** Compute the dependency graph for the ontology */ - lazy val dependencyGraph: Ontology.DependencyGraph = - Ontology.dependencyGraph(axioms, datafiles, this.unsafe) - - /** RSA check */ - lazy val isRSA: Boolean = ??? - - /** Normalize the ontology according to the given normalizer - * - * @param normalizer the normalization technique to be used. - * @return a new normalized [[Ontology]]. - */ - def normalize(normalizer: Normalizer): Ontology = - new Ontology( - axioms flatMap normalizer.normalize, - datafiles - ) - - /** Approximate the ontology according to the given approximation - * technique. - * - * @param approximation the approximation to be used on the ontology. - * @return the result of the approximation. - */ - def approximate[T](approximation: Approximation[T]): T = - approximation.approximate(this) -} diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/ontology/Ontology.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/ontology/Ontology.scala new file mode 100644 index 0000000..d73704f --- /dev/null +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/ontology/Ontology.scala @@ -0,0 +1,299 @@ +/* + * Copyright 2020, 2021 KRR Oxford + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.ac.ox.cs.rsacomb.ontology + +import java.io.File +import java.util.stream.Collectors + +import scala.collection.mutable.Map +import scala.collection.JavaConverters._ +import scalax.collection.Graph +import scalax.collection.GraphPredef._, scalax.collection.GraphEdge._ + +import org.semanticweb.owlapi.model.parameters.Imports +import org.semanticweb.owlapi.apibinding.OWLManager +import org.semanticweb.owlapi.model.{OWLOntology, OWLAxiom, OWLLogicalAxiom} +import org.semanticweb.owlapi.model.{OWLObjectPropertyExpression} +import org.semanticweb.owlapi.reasoner.structural.StructuralReasonerFactory +import tech.oxfordsemantic.jrdfox.logic.datalog.Rule +import tech.oxfordsemantic.jrdfox.logic.expression.{Resource, Term, Variable} + +import uk.ac.ox.cs.rsacomb.approximation.Approximation +import uk.ac.ox.cs.rsacomb.converter._ +import uk.ac.ox.cs.rsacomb.suffix._ +import uk.ac.ox.cs.rsacomb.util.{RDFoxUtil, RSA} + +import uk.ac.ox.cs.rsacomb.RSAUtil + +object Ontology { + + /** Simplify conversion between Java and Scala collections */ + import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ + + /** Type wrapper representing a dependency graph for the ontology. + * + * The graph is returned along with a map associating each node (IRI + * string of the resource), with the corresponding axiom in the + * original TBox. + */ + type DependencyGraph = (Graph[Resource, DiEdge], Map[String, OWLAxiom]) + + /** Manager instance to interface with OWLAPI + * + * TODO: turn this into an implicit class parameter. + */ + val manager = OWLManager.createOWLOntologyManager() + + /** Compute the RSA dependency graph for a set of axioms + * + * @param axioms set of input axioms (TBox) to build the dependency + * graph. + * @param datafiles data (ABox) to build the dependency graph. + * @param unsafe list of unsafe roles in the TBox. + * + * @return a tuple containing the dependency graph and a map between + * the newly introduced constants and the corresponding input axioms. + * + * @note no check on the ontology language is performed since the + * construction of the dependency graph is computed regardless. The + * input axioms are assumed to be normalized. + */ + def dependencyGraph( + axioms: List[OWLLogicalAxiom], + datafiles: List[File], + unsafe: List[OWLObjectPropertyExpression] + ): DependencyGraph = { + + var nodemap = Map.empty[String, OWLAxiom] + + /* Create custom converter */ + object RSAConverter extends RDFoxConverter { + + import org.semanticweb.owlapi.model.{ + OWLClassExpression, + OWLObjectSomeValuesFrom, + OWLDataSomeValuesFrom + } + + override def convert( + expr: OWLClassExpression, + term: Term, + unsafe: List[OWLObjectPropertyExpression], + skolem: SkolemStrategy, + suffix: RSASuffix + ): Shards = + (expr, skolem) match { + + case (e: OWLObjectSomeValuesFrom, c: Constant) => { + nodemap.update(c.iri.getIRI, c.axiom) + val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) + if (unsafe contains e.getProperty) + (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) + else + (RSA.PE(term, c.iri) :: res, ext) + } + + case (e: OWLDataSomeValuesFrom, c: Constant) => { + nodemap.update(c.iri.getIRI, c.axiom) + val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) + if (unsafe contains e.getProperty) + (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) + else + (RSA.PE(term, c.iri) :: res, ext) + } + + case _ => super.convert(expr, term, unsafe, skolem, suffix) + } + } + + /* Ontology convertion into LP rules */ + val term = RSAUtil.genFreshVariable() + val result = axioms.map(a => + RSAConverter.convert(a, term, unsafe, new Constant(a), Empty) + ) + + val datalog = result.unzip + val facts = datalog._1.flatten + var rules = datalog._2.flatten + + /* Open connection with RDFox */ + val (server, data) = RDFoxUtil.openConnection("rsa_dependency_graph") + + /* Add additional built-in rules */ + val varX = Variable.create("X") + val varY = Variable.create("Y") + rules = Rule.create( + RSA.E(varX, varY), + RSA.PE(varX, varY), + RSA.U(varX), + RSA.U(varY) + ) :: rules + /* Load facts and rules from ontology */ + RDFoxUtil.addFacts(data, facts) + RDFoxUtil.addRules(data, rules) + /* Load data files */ + RDFoxUtil.addData(data, datafiles: _*) + + /* Build the graph */ + val query = "SELECT ?X ?Y WHERE { ?X rsa:E ?Y }" + val answers = RDFoxUtil.submitQuery(data, query, RSA.Prefixes).get + var edges: Seq[DiEdge[Resource]] = + answers.collect { case (_, Seq(n1, n2)) => n1 ~> n2 } + val graph = Graph(edges: _*) + + /* Close connection to RDFox */ + RDFoxUtil.closeConnection(server, data) + + (graph, nodemap) + } + + def apply(axioms: List[OWLLogicalAxiom], datafiles: List[File]): Ontology = + new Ontology(axioms, datafiles) + + def apply(ontology: OWLOntology, datafiles: List[File]): Ontology = { + + /** TBox axioms */ + var tbox: List[OWLLogicalAxiom] = + ontology + .tboxAxioms(Imports.INCLUDED) + .collect(Collectors.toList()) + .collect { case a: OWLLogicalAxiom => a } + + /** RBox axioms */ + var rbox: List[OWLLogicalAxiom] = + ontology + .rboxAxioms(Imports.INCLUDED) + .collect(Collectors.toList()) + .collect { case a: OWLLogicalAxiom => a } + + /** ABox axioms + * + * @note this represents only the set of assertions contained in the + * ontology file. Data files specified in `datafiles` are directly + * imported in RDFox due to performance issues when trying to import + * large data files via OWLAPI. + */ + var abox: List[OWLLogicalAxiom] = + ontology + .aboxAxioms(Imports.INCLUDED) + .collect(Collectors.toList()) + .collect { case a: OWLLogicalAxiom => a } + + Ontology(abox ::: tbox ::: rbox, datafiles) + } + + def apply(ontofile: File, datafiles: List[File]): Ontology = { + val ontology = manager.loadOntologyFromOntologyDocument(ontofile) + Ontology(ontology, datafiles) + } + +} + +/** A wrapper for a generic OWL2 ontology + * + * @param axioms list of axioms (roughly) corresponding to the TBox. + * @param datafiles files containing ABox data. + */ +class Ontology(val axioms: List[OWLLogicalAxiom], val datafiles: List[File]) { + + /** Extend OWLAxiom functionalities */ + import uk.ac.ox.cs.rsacomb.implicits.RSAAxiom._ + + /** Simplify conversion between Java and Scala collections */ + import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ + + /** OWLOntology based on input axioms + * + * This is mainly used to instantiate a new reasoner to be used in + * the computation of unsafe roles. + */ + protected val ontology: OWLOntology = + Ontology.manager.createOntology((axioms: List[OWLAxiom]).asJava) + + /** OWLAPI internal reasoner for ontology */ + protected val reasoner = + (new StructuralReasonerFactory()).createReasoner(ontology) + + /** Unsafe roles in the ontology + * + * Unsafety conditions are the following: + * + * 1) For all roles r1 appearing in an axiom of type T5, r1 is unsafe + * if there exists a role r2 (different from top) appearing in an + * axiom of type T3 and r1 is a subproperty of the inverse of r2. + * + * 2) For all roles p1 appearing in an axiom of type T5, p1 is unsafe + * if there exists a role p2 appearing in an axiom of type T4 and + * p1 is a subproperty of either p2 or the inverse of p2. + */ + lazy val unsafe: List[OWLObjectPropertyExpression] = { + + /* Checking for unsafety condition (1) */ + val unsafe1 = for { + axiom <- axioms + if axiom.isT5 + role1 <- axiom.objectPropertyExpressionsInSignature + roleSuper = role1 +: reasoner.superObjectProperties(role1) + axiom <- axioms + if axiom.isT3 && !axiom.isT3top + role2 <- axiom.objectPropertyExpressionsInSignature + if roleSuper contains role2.getInverseProperty + } yield role1 + + /* Checking for unsafety condition (2) */ + val unsafe2 = for { + axiom <- axioms + if axiom.isT5 + role1 <- axiom.objectPropertyExpressionsInSignature + roleSuper = role1 +: reasoner.superObjectProperties(role1) + axiom <- axioms + if axiom.isT4 + role2 <- axiom.objectPropertyExpressionsInSignature + if roleSuper.contains(role2) || + roleSuper.contains(role2.getInverseProperty) + } yield role1 + + unsafe1 ++ unsafe2 + } + + /** Compute the dependency graph for the ontology */ + lazy val dependencyGraph: Ontology.DependencyGraph = + Ontology.dependencyGraph(axioms, datafiles, this.unsafe) + + /** RSA check */ + lazy val isRSA: Boolean = ??? + + /** Normalize the ontology according to the given normalizer + * + * @param normalizer the normalization technique to be used. + * @return a new normalized [[Ontology]]. + */ + def normalize(normalizer: Normalizer): Ontology = + new Ontology( + axioms flatMap normalizer.normalize, + datafiles + ) + + /** Approximate the ontology according to the given approximation + * technique. + * + * @param approximation the approximation to be used on the ontology. + * @return the result of the approximation. + */ + def approximate[T](approximation: Approximation[T]): T = + approximation.approximate(this) +} -- cgit v1.2.3 From 7d619706551117a485d93d0d6847a25afa6a359d Mon Sep 17 00:00:00 2001 From: Federico Igne Date: Tue, 27 Jul 2021 10:30:07 +0100 Subject: Rework class structure to match more general workflow The major change is the introduction of a new class Ontology (superclass of RSAOntology) that contains all those operation that we would like to perform on *any* OWL 2 ontology. Approximation is also generic on the return type, allowing for example intermediate steps (or chaining of approximations). --- src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala | 24 +++++++++++----------- .../ox/cs/rsacomb/approximation/Lowerbound.scala | 2 +- .../uk/ac/ox/cs/rsacomb/ontology/Ontology.scala | 2 ++ 3 files changed, 15 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala index 8e5169d..713a9e8 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala @@ -131,18 +131,18 @@ object RSAComb extends App { Logger.print(s"$answers", Logger.VERBOSE) Logger print s"Number of answers: ${answers.length} (${answers.lengthWithMultiplicity})" // Retrieve unfiltered answers - val unfiltered = rsa.queryDataStore( - """ - SELECT (count(?K) as ?COUNT) - WHERE { - ?K a rsa:QM . - } - """, - RSA.Prefixes - ) - unfiltered.foreach((u) => - Logger print s"Number of unfiltered answers: ${u.head._2}" - ) + // val unfiltered = rsa.queryDataStore( + // """ + // SELECT (count(?K) as ?COUNT) + // WHERE { + // ?K a rsa:QM . + // } + // """, + // RSA.Prefixes + // ) + // unfiltered.foreach((u) => + // Logger print s"Number of unfiltered answers: ${u.head._2}" + // ) } case None => throw new RuntimeException("Submitted query is not conjunctive") diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/Lowerbound.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/Lowerbound.scala index 3fc4988..60a88fb 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/Lowerbound.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/Lowerbound.scala @@ -50,7 +50,7 @@ class LowerBound extends Approximation[RSAOntology] { def approximate(ontology: Ontology): RSAOntology = toRSA( new Ontology( - ontology.axioms filterNot inALCHOIQ flatMap shift, + ontology.axioms filter inALCHOIQ flatMap shift, ontology.datafiles ) ) diff --git a/src/main/scala/uk/ac/ox/cs/rsacomb/ontology/Ontology.scala b/src/main/scala/uk/ac/ox/cs/rsacomb/ontology/Ontology.scala index d73704f..ba44605 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/ontology/Ontology.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/ontology/Ontology.scala @@ -216,6 +216,8 @@ class Ontology(val axioms: List[OWLLogicalAxiom], val datafiles: List[File]) { /** Simplify conversion between Java and Scala collections */ import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ + println(s"Axioms: ${axioms.length}") + /** OWLOntology based on input axioms * * This is mainly used to instantiate a new reasoner to be used in -- cgit v1.2.3