aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/main/scala/uk/ac/ox/cs/rsacomb/Main.scala7
-rw-r--r--src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala481
-rw-r--r--src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala19
-rw-r--r--src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala241
-rw-r--r--src/main/scala/uk/ac/ox/cs/rsacomb/converter/Normalizer.scala25
5 files changed, 532 insertions, 241 deletions
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 f9de801..b749401 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 {
106 /* Command-line options */ 106 /* Command-line options */
107 val config = RSAConfig.parse(args.toList) 107 val config = RSAConfig.parse(args.toList)
108 108
109 val ontology = RSAOntology( 109 val rsa = RSAOntology(
110 config('ontology).get[File], 110 config('ontology).get[File],
111 config('data).get[List[File]]: _* 111 config('data).get[List[File]],
112 None
112 ) 113 )
113 val rsa = ontology.toRSA()
114 ontology.statistics()
115 114
116 if (config contains 'query) { 115 if (config contains 'query) {
117 val query = 116 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 0e10f89..bbbbcf3 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._
50import scala.collection.mutable.{Set, Map} 50import scala.collection.mutable.{Set, Map}
51import scalax.collection.Graph 51import scalax.collection.Graph
52import scalax.collection.GraphPredef._, scalax.collection.GraphEdge._ 52import scalax.collection.GraphPredef._, scalax.collection.GraphEdge._
53import scalax.collection.GraphTraversal._
54 53
55/* Debug only */ 54/* Debug only */
56import org.semanticweb.owlapi.dlsyntax.renderer.DLSyntaxObjectRenderer 55import org.semanticweb.owlapi.dlsyntax.renderer.DLSyntaxObjectRenderer
57import tech.oxfordsemantic.jrdfox.logic._ 56import tech.oxfordsemantic.jrdfox.logic._
58import org.semanticweb.owlapi.model.OWLObjectInverseOf 57import org.semanticweb.owlapi.model.OWLObjectInverseOf
59 58
59import uk.ac.ox.cs.rsacomb.approximation.Approximation
60import uk.ac.ox.cs.rsacomb.converter._ 60import uk.ac.ox.cs.rsacomb.converter._
61import uk.ac.ox.cs.rsacomb.filtering.{FilteringProgram, FilterType} 61import uk.ac.ox.cs.rsacomb.filtering.{FilteringProgram, FilterType}
62import uk.ac.ox.cs.rsacomb.suffix._ 62import uk.ac.ox.cs.rsacomb.suffix._
@@ -64,29 +64,189 @@ import uk.ac.ox.cs.rsacomb.sparql._
64import uk.ac.ox.cs.rsacomb.util.{RDFoxUtil, RSA} 64import uk.ac.ox.cs.rsacomb.util.{RDFoxUtil, RSA}
65import uk.ac.ox.cs.rsacomb.util.Logger 65import uk.ac.ox.cs.rsacomb.util.Logger
66 66
67object RSAUtil {
68
69 // implicit def axiomsToOntology(axioms: Seq[OWLAxiom]) = {
70 // val manager = OWLManager.createOWLOntologyManager()
71 // manager.createOntology(axioms.asJava)
72 // }
73
74 /** Compute the RSA dependency graph for a set of axioms
75 *
76 * @return a tuple containing the dependency graph and a map between
77 * the newly introduced constants and the corresponding input axioms.
78 *
79 * @note no check on the ontology language is performed since the
80 * construction of the dependency graph is computed regardless. The
81 * input axioms are assumed to be normalized.
82 */
83 def dependencyGraph(
84 axioms: List[OWLLogicalAxiom],
85 datafiles: List[File]
86 ): (Graph[Resource, DiEdge], Map[String, OWLAxiom]) = {
87 val unsafe = RSAOntology(axioms, datafiles).unsafeRoles
88 var nodemap = Map.empty[String, OWLAxiom]
89
90 object RSAConverter extends RDFoxConverter {
91
92 override def convert(
93 expr: OWLClassExpression,
94 term: Term,
95 unsafe: List[OWLObjectPropertyExpression],
96 skolem: SkolemStrategy,
97 suffix: RSASuffix
98 ): Shards =
99 (expr, skolem) match {
100
101 case (e: OWLObjectSomeValuesFrom, c: Constant) => {
102 nodemap.update(c.iri.getIRI, c.axiom)
103 val (res, ext) = super.convert(e, term, unsafe, skolem, suffix)
104 if (unsafe contains e.getProperty)
105 (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext)
106 else
107 (RSA.PE(term, c.iri) :: res, ext)
108 }
109
110 case (e: OWLDataSomeValuesFrom, c: Constant) => {
111 nodemap.update(c.iri.getIRI, c.axiom)
112 val (res, ext) = super.convert(e, term, unsafe, skolem, suffix)
113 if (unsafe contains e.getProperty)
114 (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext)
115 else
116 (RSA.PE(term, c.iri) :: res, ext)
117 }
118
119 case _ => super.convert(expr, term, unsafe, skolem, suffix)
120 }
121 }
122
123 /* Ontology convertion into LP rules */
124 val term = RSAOntology.genFreshVariable()
125 val result = axioms.map(a =>
126 RSAConverter.convert(a, term, unsafe, new Constant(a), Empty)
127 )
128
129 val datalog = result.unzip
130 val facts = datalog._1.flatten
131 var rules = datalog._2.flatten
132
133 /* Open connection with RDFox */
134 val (server, data) = RDFoxUtil.openConnection("rsa_dependency_graph")
135
136 /* Add additional built-in rules */
137 val varX = Variable.create("X")
138 val varY = Variable.create("Y")
139 rules = Rule.create(
140 RSA.E(varX, varY),
141 RSA.PE(varX, varY),
142 RSA.U(varX),
143 RSA.U(varY)
144 ) :: rules
145 /* Load facts and rules from ontology */
146 RDFoxUtil.addFacts(data, facts)
147 RDFoxUtil.addRules(data, rules)
148 /* Load data files */
149 RDFoxUtil.addData(data, datafiles: _*)
150
151 /* Build the graph */
152 val query = "SELECT ?X ?Y WHERE { ?X rsa:E ?Y }"
153 val answers = RDFoxUtil.submitQuery(data, query, RSA.Prefixes).get
154 var edges: Seq[DiEdge[Resource]] =
155 answers.collect { case (_, Seq(n1, n2)) => n1 ~> n2 }
156 val graph = Graph(edges: _*)
157
158 /* Close connection to RDFox */
159 RDFoxUtil.closeConnection(server, data)
160
161 (graph, nodemap)
162 }
163}
164
67object RSAOntology { 165object RSAOntology {
68 166
167 import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._
168
169 /** Manager instance to interface with OWLAPI */
170 val manager = OWLManager.createOWLOntologyManager()
171 val factory = manager.getOWLDataFactory()
172
69 /** Name of the RDFox data store used for CQ answering */ 173 /** Name of the RDFox data store used for CQ answering */
70 private val DataStore = "answer_computation" 174 private val DataStore = "answer_computation"
71 175
72 /** Simple fresh variable generator */ 176 /** Simple fresh variable/class generator */
73 private var counter = -1; 177 private var counter = -1;
74 def genFreshVariable(): Variable = { 178 def genFreshVariable(): Variable = {
75 counter += 1 179 counter += 1
76 Variable.create(f"I$counter%05d") 180 Variable.create(f"I$counter%05d")
77 } 181 }
182 def getFreshOWLClass(): OWLClass = {
183 counter += 1
184 factory.getOWLClass(s"X$counter")
185 }
78 186
79 /** Manager instance to interface with OWLAPI */ 187 def apply(
80 val manager = OWLManager.createOWLOntologyManager() 188 axioms: List[OWLLogicalAxiom],
189 datafiles: List[File]
190 ): RSAOntology = new RSAOntology(axioms, datafiles: _*)
191
192 def apply(
193 ontofile: File,
194 datafiles: List[File],
195 approx: Option[Approximation]
196 ): RSAOntology = {
197 val ontology = manager.loadOntologyFromOntologyDocument(ontofile)
198 RSAOntology(ontology, datafiles, approx)
199 }
200
201 def apply(
202 ontology: OWLOntology,
203 datafiles: List[File],
204 approx: Option[Approximation]
205 ): RSAOntology = {
206 val normalizer = new Normalizer()
207
208 /** TBox axioms */
209 var tbox: List[OWLLogicalAxiom] =
210 ontology
211 .tboxAxioms(Imports.INCLUDED)
212 .collect(Collectors.toList())
213 .collect { case a: OWLLogicalAxiom => a }
214 .flatMap(normalizer.normalize)
215
216 /** RBox axioms */
217 var rbox: List[OWLLogicalAxiom] =
218 ontology
219 .rboxAxioms(Imports.INCLUDED)
220 .collect(Collectors.toList())
221 .collect { case a: OWLLogicalAxiom => a }
222 .flatMap(normalizer.normalize)
223
224 /** ABox axioms
225 *
226 * @note this represents only the set of assertions contained in the
227 * ontology file. Data files specified in `datafiles` are directly
228 * imported in RDFox due to performance issues when trying to import
229 * large data files via OWLAPI.
230 */
231 var abox: List[OWLLogicalAxiom] =
232 ontology
233 .aboxAxioms(Imports.INCLUDED)
234 .collect(Collectors.toList())
235 .collect { case a: OWLLogicalAxiom => a }
236 .flatMap(normalizer.normalize)
237
238 /** Collection of logical axioms in the input ontology */
239 var axioms: List[OWLLogicalAxiom] = abox ::: tbox ::: rbox
81 240
82 def apply(ontology: File, data: File*): RSAOntology =
83 new RSAOntology( 241 new RSAOntology(
84 manager.loadOntologyFromOntologyDocument(ontology), 242 approx match {
85 data: _* 243 case Some(a) => a.approximate(axioms, datafiles)
244 case None => axioms
245 },
246 datafiles: _*
86 ) 247 )
248 }
87 249
88 def apply(ontology: OWLOntology, data: File*): RSAOntology =
89 new RSAOntology(ontology, data: _*)
90} 250}
91 251
92/** Wrapper class for an ontology in RSA 252/** Wrapper class for an ontology in RSA
@@ -94,59 +254,21 @@ object RSAOntology {
94 * @param ontology the input OWL2 ontology. 254 * @param ontology the input OWL2 ontology.
95 * @param datafiles additinal data (treated as part of the ABox) 255 * @param datafiles additinal data (treated as part of the ABox)
96 */ 256 */
97class RSAOntology(val original: OWLOntology, val datafiles: File*) { 257class RSAOntology(val axioms: List[OWLLogicalAxiom], val datafiles: File*) {
98 258
99 /** Simplify conversion between OWLAPI and RDFox concepts */ 259 /** Simplify conversion between OWLAPI and RDFox concepts */
100 import implicits.RDFox._ 260 import implicits.RDFox._
101 import uk.ac.ox.cs.rsacomb.implicits.RSAAxiom._ 261 import uk.ac.ox.cs.rsacomb.implicits.RSAAxiom._
262
263 /** Simplify conversion between Java and Scala collections */
102 import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ 264 import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._
103 265
104 /** Set of axioms removed during the approximation to RSA */ 266 /** Set of axioms removed during the approximation to RSA */
105 private var removed: Seq[OWLAxiom] = Seq.empty 267 private var removed: Seq[OWLAxiom] = Seq.empty
106 268
107 /** The normalizer normalizes the ontology and approximate it to
108 * Horn-ALCHOIQ. A further step is needed to obtain an RSA
109 * approximation of the input ontology `original`.
110 */
111 private val normalizer = new Normalizer()
112
113 /** TBox axioms */
114 var tbox: List[OWLLogicalAxiom] =
115 original
116 .tboxAxioms(Imports.INCLUDED)
117 .collect(Collectors.toList())
118 .collect { case a: OWLLogicalAxiom => a }
119 .flatMap(normalizer.normalize)
120
121 /** RBox axioms */
122 var rbox: List[OWLLogicalAxiom] =
123 original
124 .rboxAxioms(Imports.INCLUDED)
125 .collect(Collectors.toList())
126 .collect { case a: OWLLogicalAxiom => a }
127 .flatMap(normalizer.normalize)
128
129 /** ABox axioms
130 *
131 * @note this represents only the set of assertions contained in the
132 * ontology file. Data files specified in `datafiles` are directly
133 * imported in RDFox due to performance issues when trying to import
134 * large data files via OWLAPI.
135 */
136 var abox: List[OWLLogicalAxiom] =
137 original
138 .aboxAxioms(Imports.INCLUDED)
139 .collect(Collectors.toList())
140 .collect { case a: OWLLogicalAxiom => a }
141 .flatMap(normalizer.normalize)
142
143 /** Collection of logical axioms in the input ontology */
144 var axioms: List[OWLLogicalAxiom] = abox ::: tbox ::: rbox
145
146 /** Normalized Horn-ALCHOIQ ontology */ 269 /** Normalized Horn-ALCHOIQ ontology */
147 val ontology = RSAOntology.manager.createOntology( 270 val ontology =
148 axioms.asInstanceOf[List[OWLAxiom]].asJava 271 RSAOntology.manager.createOntology((axioms: List[OWLAxiom]).asJava)
149 )
150 272
151 /** OWLAPI internal reasoner instantiated over the approximated ontology */ 273 /** OWLAPI internal reasoner instantiated over the approximated ontology */
152 private val reasoner = 274 private val reasoner =
@@ -170,7 +292,7 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) {
170 val concepts: List[OWLClass] = 292 val concepts: List[OWLClass] =
171 ontology.getClassesInSignature().asScala.toList 293 ontology.getClassesInSignature().asScala.toList
172 val roles: List[OWLObjectPropertyExpression] = 294 val roles: List[OWLObjectPropertyExpression] =
173 (tbox ++ rbox) 295 axioms
174 .flatMap(_.objectPropertyExpressionsInSignature) 296 .flatMap(_.objectPropertyExpressionsInSignature)
175 .distinct 297 .distinct
176 298
@@ -190,12 +312,12 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) {
190 312
191 /* Checking for unsafety condition (1) */ 313 /* Checking for unsafety condition (1) */
192 val unsafe1 = for { 314 val unsafe1 = for {
193 axiom <- tbox 315 axiom <- axioms
194 if axiom.isT5 316 if axiom.isT5
195 role1 <- axiom.objectPropertyExpressionsInSignature 317 role1 <- axiom.objectPropertyExpressionsInSignature
196 roleSuper = role1 +: reasoner.superObjectProperties(role1) 318 roleSuper = role1 +: reasoner.superObjectProperties(role1)
197 roleSuperInv = roleSuper.map(_.getInverseProperty) 319 roleSuperInv = roleSuper.map(_.getInverseProperty)
198 axiom <- tbox 320 axiom <- axioms
199 if axiom.isT3 && !axiom.isT3top 321 if axiom.isT3 && !axiom.isT3top
200 role2 <- axiom.objectPropertyExpressionsInSignature 322 role2 <- axiom.objectPropertyExpressionsInSignature
201 if roleSuperInv contains role2 323 if roleSuperInv contains role2
@@ -203,12 +325,12 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) {
203 325
204 /* Checking for unsafety condition (2) */ 326 /* Checking for unsafety condition (2) */
205 val unsafe2 = for { 327 val unsafe2 = for {
206 axiom <- tbox 328 axiom <- axioms
207 if axiom.isT5 329 if axiom.isT5
208 role1 <- axiom.objectPropertyExpressionsInSignature 330 role1 <- axiom.objectPropertyExpressionsInSignature
209 roleSuper = role1 +: reasoner.superObjectProperties(role1) 331 roleSuper = role1 +: reasoner.superObjectProperties(role1)
210 roleSuperInv = roleSuper.map(_.getInverseProperty) 332 roleSuperInv = roleSuper.map(_.getInverseProperty)
211 axiom <- tbox 333 axiom <- axioms
212 if axiom.isT4 334 if axiom.isT4
213 role2 <- axiom.objectPropertyExpressionsInSignature 335 role2 <- axiom.objectPropertyExpressionsInSignature
214 if roleSuper.contains(role2) || roleSuperInv.contains(role2) 336 if roleSuper.contains(role2) || roleSuperInv.contains(role2)
@@ -217,93 +339,6 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) {
217 unsafe1 ++ unsafe2 339 unsafe1 ++ unsafe2
218 } 340 }
219 341
220 /** Compute the RSA dependency graph
221 *
222 * This is used to approximate the input ontology to RSA.
223 *
224 * @return a tuple containing the dependency graph and a map between
225 * the constants newly introduced and the corresponding axioms in the
226 * ontology.
227 */
228 private def dependencyGraph()
229 : (Graph[Resource, DiEdge], Map[String, OWLAxiom]) = {
230 val unsafe = this.unsafeRoles
231 var nodemap = Map.empty[String, OWLAxiom]
232
233 object RSAConverter extends RDFoxConverter {
234
235 override def convert(
236 expr: OWLClassExpression,
237 term: Term,
238 unsafe: List[OWLObjectPropertyExpression],
239 skolem: SkolemStrategy,
240 suffix: RSASuffix
241 ): Shards =
242 (expr, skolem) match {
243
244 case (e: OWLObjectSomeValuesFrom, c: Constant) => {
245 nodemap.update(c.iri.getIRI, c.axiom)
246 val (res, ext) = super.convert(e, term, unsafe, skolem, suffix)
247 if (unsafe contains e.getProperty)
248 (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext)
249 else
250 (RSA.PE(term, c.iri) :: res, ext)
251 }
252
253 case (e: OWLDataSomeValuesFrom, c: Constant) => {
254 nodemap.update(c.iri.getIRI, c.axiom)
255 val (res, ext) = super.convert(e, term, unsafe, skolem, suffix)
256 if (unsafe contains e.getProperty)
257 (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext)
258 else
259 (RSA.PE(term, c.iri) :: res, ext)
260 }
261
262 case _ => super.convert(expr, term, unsafe, skolem, suffix)
263 }
264 }
265
266 /* Ontology convertion into LP rules */
267 val term = RSAOntology.genFreshVariable()
268 val result = axioms.map(a =>
269 RSAConverter.convert(a, term, unsafe, new Constant(a), Empty)
270 )
271
272 val datalog = result.unzip
273 val facts = datalog._1.flatten
274 var rules = datalog._2.flatten
275
276 /* Open connection with RDFox */
277 val (server, data) = RDFoxUtil.openConnection("rsa_dependency_graph")
278
279 /* Add additional built-in rules */
280 val varX = Variable.create("X")
281 val varY = Variable.create("Y")
282 rules = Rule.create(
283 RSA.E(varX, varY),
284 RSA.PE(varX, varY),
285 RSA.U(varX),
286 RSA.U(varY)
287 ) :: rules
288 /* Load facts and rules from ontology */
289 RDFoxUtil.addFacts(data, facts)
290 RDFoxUtil.addRules(data, rules)
291 /* Load data files */
292 RDFoxUtil.addData(data, datafiles: _*)
293
294 /* Build the graph */
295 val query = "SELECT ?X ?Y WHERE { ?X rsa:E ?Y }"
296 val answers = RDFoxUtil.submitQuery(data, query, RSA.Prefixes).get
297 var edges: Seq[DiEdge[Resource]] =
298 answers.collect { case (_, Seq(n1, n2)) => n1 ~> n2 }
299 val graph = Graph(edges: _*)
300
301 /* Close connection to RDFox */
302 RDFoxUtil.closeConnection(server, data)
303
304 (graph, nodemap)
305 }
306
307 /** Approximate a Horn-ALCHOIQ ontology to RSA 342 /** Approximate a Horn-ALCHOIQ ontology to RSA
308 * 343 *
309 * This is done by gathering those axioms that prevent the ontology 344 * This is done by gathering those axioms that prevent the ontology
@@ -313,61 +348,61 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) {
313 * @param graph the graph used to compute the axioms to remove. 348 * @param graph the graph used to compute the axioms to remove.
314 * @param nodemap map from graph nodes to ontology axioms. 349 * @param nodemap map from graph nodes to ontology axioms.
315 */ 350 */
316 def toRSA(): RSAOntology = Logger.timed( 351 // def toRSA(): RSAOntology = Logger.timed(
317 { 352 // {
318 353
319 /* Compute the dependency graph for the ontology */ 354 // /* Compute the dependency graph for the ontology */
320 val (graph, nodemap) = this.dependencyGraph() 355 // val (graph, nodemap) = this.dependencyGraph()
321 356
322 /* Define node colors for the graph visit */ 357 // /* Define node colors for the graph visit */
323 sealed trait NodeColor 358 // sealed trait NodeColor
324 case object Unvisited extends NodeColor 359 // case object Unvisited extends NodeColor
325 case object Visited extends NodeColor 360 // case object Visited extends NodeColor
326 case object ToDelete extends NodeColor 361 // case object ToDelete extends NodeColor
327 362
328 /* Keep track of node colors during graph visit */ 363 // /* Keep track of node colors during graph visit */
329 var color = Map.from[Resource, NodeColor]( 364 // var color = Map.from[Resource, NodeColor](
330 graph.nodes.toOuter.map(k => (k, Unvisited)) 365 // graph.nodes.toOuter.map(k => (k, Unvisited))
331 ) 366 // )
332 367
333 for { 368 // for {
334 component <- graph.componentTraverser().map(_ to Graph) 369 // component <- graph.componentTraverser().map(_ to Graph)
335 edge <- component 370 // edge <- component
336 .outerEdgeTraverser(component.nodes.head) 371 // .outerEdgeTraverser(component.nodes.head)
337 .withKind(BreadthFirst) 372 // .withKind(BreadthFirst)
338 } yield { 373 // } yield {
339 val source = edge._1 374 // val source = edge._1
340 val target = edge._2 375 // val target = edge._2
341 color(source) match { 376 // color(source) match {
342 case Unvisited | Visited => { 377 // case Unvisited | Visited => {
343 color(target) match { 378 // color(target) match {
344 case Unvisited => 379 // case Unvisited =>
345 color(source) = Visited; 380 // color(source) = Visited;
346 color(target) = Visited 381 // color(target) = Visited
347 case Visited => 382 // case Visited =>
348 color(source) = ToDelete 383 // color(source) = ToDelete
349 case ToDelete => 384 // case ToDelete =>
350 color(source) = Visited 385 // color(source) = Visited
351 } 386 // }
352 } 387 // }
353 case ToDelete => 388 // case ToDelete =>
354 } 389 // }
355 } 390 // }
356 391
357 val toDelete = color.iterator.collect { case (resource: IRI, ToDelete) => 392 // val toDelete = color.iterator.collect { case (resource: IRI, ToDelete) =>
358 nodemap(resource.getIRI) 393 // nodemap(resource.getIRI)
359 }.toSeq 394 // }.toSeq
360 395
361 /* Remove axioms from approximated ontology */ 396 // /* Remove axioms from approximated ontology */
362 ontology.removeAxioms(toDelete: _*) 397 // ontology.removeAxioms(toDelete: _*)
363 this.removed = toDelete 398 // this.removed = toDelete
364 399
365 /* Return RSA ontology */ 400 // /* Return RSA ontology */
366 RSAOntology(ontology, datafiles: _*) 401 // RSAOntology(ontology, datafiles: _*)
367 }, 402 // },
368 "Horn-ALCHOIQ to RSA approximation:", 403 // "Horn-ALCHOIQ to RSA approximation:",
369 Logger.DEBUG 404 // Logger.DEBUG
370 ) 405 // )
371 // val edges1 = Seq('A ~> 'B, 'B ~> 'C, 'C ~> 'D, 'D ~> 'H, 'H ~> 406 // val edges1 = Seq('A ~> 'B, 'B ~> 'C, 'C ~> 'D, 'D ~> 'H, 'H ~>
372 // 'G, 'G ~> 'F, 'E ~> 'A, 'E ~> 'F, 'B ~> 'E, 'F ~> 'G, 'B ~> 'F, 407 // 'G, 'G ~> 'F, 'E ~> 'A, 'E ~> 'F, 'B ~> 'E, 'F ~> 'G, 'B ~> 'F,
373 // 'C ~> 'G, 'D ~> 'C, 'H ~> 'D) 408 // 'C ~> 'G, 'D ~> 'C, 'H ~> 'D)
@@ -651,7 +686,7 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) {
651 val conflR = this.confl(roleR) 686 val conflR = this.confl(roleR)
652 // We just need the TBox to find 687 // We just need the TBox to find
653 val terms = for { 688 val terms = for {
654 axiom1 <- tbox 689 axiom1 <- axioms
655 if axiom1.isT5 690 if axiom1.isT5
656 // We expect only one role coming out of a T5 axiom 691 // We expect only one role coming out of a T5 axiom
657 roleS <- axiom1.objectPropertyExpressionsInSignature 692 roleS <- axiom1.objectPropertyExpressionsInSignature
@@ -676,28 +711,28 @@ class RSAOntology(val original: OWLOntology, val datafiles: File*) {
676 this.self(axiom) | this.cycle(axiom) 711 this.self(axiom) | this.cycle(axiom)
677 712
678 /** Log normalization/approximation statistics */ 713 /** Log normalization/approximation statistics */
679 def statistics(level: Logger.Level = Logger.DEBUG): Unit = { 714 // def statistics(level: Logger.Level = Logger.DEBUG): Unit = {
680 Logger.print( 715 // Logger.print(
681 s"Logical axioms in original input ontology: ${original.getLogicalAxiomCount(true)}", 716 // s"Logical axioms in original input ontology: ${original.getLogicalAxiomCount(true)}",
682 level 717 // level
683 ) 718 // )
684 Logger.print( 719 // Logger.print(
685 s"Logical axioms discarded in Horn-ALCHOIQ approximation: ${normalizer.discarded}", 720 // s"Logical axioms discarded in Horn-ALCHOIQ approximation: ${normalizer.discarded}",
686 level 721 // level
687 ) 722 // )
688 Logger.print( 723 // Logger.print(
689 s"Logical axioms shifted in Horn-ALCHOIQ approximation: ${normalizer.shifted}", 724 // s"Logical axioms shifted in Horn-ALCHOIQ approximation: ${normalizer.shifted}",
690 level 725 // level
691 ) 726 // )
692 Logger.print( 727 // Logger.print(
693 s"Logical axioms in Horn-ALCHOIQ ontology: ${ontology 728 // s"Logical axioms in Horn-ALCHOIQ ontology: ${ontology
694 .getLogicalAxiomCount(true)} (${tbox.length}/${rbox.length}/${abox.length})", 729 // .getLogicalAxiomCount(true)} (${axioms.length}/${axioms.length}/${axioms.length})",
695 level 730 // level
696 ) 731 // )
697 Logger.print( 732 // Logger.print(
698 s"Logical axioms discarded in RSA approximation: ${removed.length}", 733 // s"Logical axioms discarded in RSA approximation: ${removed.length}",
699 level 734 // level
700 ) 735 // )
701 } 736 // }
702 737
703} // class RSAOntology 738} // 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
new file mode 100644
index 0000000..1b49413
--- /dev/null
+++ b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/approximation.scala
@@ -0,0 +1,19 @@
1package uk.ac.ox.cs.rsacomb.approximation
2
3import java.io.File
4import org.semanticweb.owlapi.model.OWLLogicalAxiom
5
6/** Ontology approximation technique. */
7trait Approximation {
8
9 /** Approximate an ontology.
10 *
11 * @param ontology input ontology
12 * @return a new approximated ontology
13 */
14 def approximate(
15 ontology: List[OWLLogicalAxiom],
16 datafiles: List[File]
17 ): List[OWLLogicalAxiom]
18
19}
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..8a86d19
--- /dev/null
+++ b/src/main/scala/uk/ac/ox/cs/rsacomb/approximation/lowerbound.scala
@@ -0,0 +1,241 @@
1package uk.ac.ox.cs.rsacomb.approximation
2
3import java.io.File
4
5import org.semanticweb.owlapi.apibinding.OWLManager
6import org.semanticweb.owlapi.model.{IRI => _, _}
7
8import tech.oxfordsemantic.jrdfox.logic.expression.{Resource, IRI}
9
10import scala.collection.mutable.{Set, Map}
11import scalax.collection.Graph
12import scalax.collection.GraphPredef._, scalax.collection.GraphEdge._
13import scalax.collection.GraphTraversal._
14
15import uk.ac.ox.cs.rsacomb.RSAOntology
16import uk.ac.ox.cs.rsacomb.RSAUtil
17import uk.ac.ox.cs.rsacomb.converter.Normalizer
18
19/** Approximation algorithm that mantains soundness for CQ answering.
20 *
21 * The input OWL 2 ontology is assumed to be normalized and the output
22 * ontology is guaranteed to be in RSA.
23 *
24 * The algorithm is performed in three steps:
25 * 1. the ontology is reduced to ALCHOIQ by discarding any axiom
26 * that is not in the language;
27 * 2. the ontology is further reduced to Horn-ALCHOIQ by shifting
28 * axioms with disjunction on the rhs;
29 * 3. the ontology is approximated to RSA by manipulating its
30 * dependency graph.
31 *
32 * @see [[uk.ac.ox.cs.rsacomb.converter.Normalizer]]
33 */
34class LowerBound extends Approximation {
35
36 /** Simplify conversion between Java and Scala collections */
37 import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._
38
39 /** Simplify conversion between OWLAPI and RDFox concepts */
40 import uk.ac.ox.cs.rsacomb.implicits.RDFox._
41
42 /** Manager instance to interface with OWLAPI */
43 val manager = OWLManager.createOWLOntologyManager()
44 val factory = manager.getOWLDataFactory()
45
46 val normalizer = new Normalizer()
47
48 /** Main entry point for the approximation algorithm */
49 def approximate(
50 ontology: List[OWLLogicalAxiom],
51 datafiles: List[File]
52 ): List[OWLLogicalAxiom] = {
53 /* Normalize axioms */
54 val axioms1 = ontology flatMap normalizer.normalize
55 /* Delete any axiom outside of ALCHOIQ */
56 val axioms2 = axioms1 filterNot inALCHOIQ
57 /* Shift any axiom with disjunction on the rhs */
58 val axioms3 = for {
59 a1 <- axioms1
60 a2 <- shift(a1)
61 a3 <- normalizer.normalize(a2)
62 } yield a3
63 /* Approximate to RSA */
64 toRSA(axioms3, datafiles)
65 }
66
67 /** Discards all axioms outside ALCHOIQ */
68 def inALCHOIQ(axiom: OWLLogicalAxiom): Boolean =
69 axiom match {
70 case a: OWLSubClassOfAxiom => {
71 val sub = a.getSubClass.getNNF
72 val sup = a.getSuperClass.getNNF
73 (sub, sup) match {
74 case (sub: OWLObjectAllValuesFrom, _) => false
75 case (sub: OWLDataAllValuesFrom, _) => false
76 case (_, sup: OWLDataAllValuesFrom) => false
77 case (sub: OWLObjectMinCardinality, _) if sub.getCardinality >= 2 =>
78 false
79 case (sub: OWLDataMinCardinality, _) if sub.getCardinality >= 2 =>
80 false
81 case (_, sup: OWLObjectMinCardinality) if sup.getCardinality >= 2 =>
82 false
83 case (_, sup: OWLDataMinCardinality) if sup.getCardinality >= 2 =>
84 false
85 case (sub: OWLObjectMaxCardinality, _) => false
86 case (sub: OWLDataMaxCardinality, _) => false
87 case (_, sup: OWLObjectMaxCardinality) if sup.getCardinality >= 2 =>
88 false
89 case (_, sup: OWLDataMaxCardinality) if sup.getCardinality >= 1 =>
90 false
91 case (_, sup: OWLObjectOneOf) if sup.getIndividuals.length > 2 =>
92 false
93 case (sub: OWLObjectHasSelf, _) => false
94 case (_, sup: OWLObjectHasSelf) => false
95 case _ => true
96 }
97 }
98 case a: OWLTransitiveObjectPropertyAxiom => false
99 case a: OWLReflexiveObjectPropertyAxiom => false
100 case a: OWLSubPropertyChainOfAxiom => false
101 case a: OWLAsymmetricObjectPropertyAxiom => false
102 case a => true
103 }
104
105 /** Shifting axioms with disjunction on the rhs.
106 *
107 * The process of shifting presenves soundness but completenes w.r.t.
108 * CQ answering is lost.
109 *
110 * @example
111 *
112 * A -> B1 u B2 u B3 .
113 *
114 * becomes
115 *
116 * A n nB1 n nB2 -> B3 .
117 * A n nB1 n nB3 -> B2 .
118 * A n nB2 n nB3 -> B1 .
119 * nB1 n nB2 n nB3 -> nA .
120 *
121 * where nA, nB1, nB2, nB3 are fresh predicates "corresponding" to
122 * the negation of A, B1, B2, B3 respectively.
123 */
124 def shift(axiom: OWLLogicalAxiom): List[OWLLogicalAxiom] =
125 axiom match {
126 case a: OWLSubClassOfAxiom => {
127 val sub = a.getSubClass.getNNF
128 val sup = a.getSuperClass.getNNF
129 sup match {
130 case sup: OWLObjectUnionOf => {
131 val body = sub.asConjunctSet.map((atom) =>
132 (atom, RSAOntology.getFreshOWLClass())
133 )
134 val head = sup.asDisjunctSet.map((atom) =>
135 (atom, RSAOntology.getFreshOWLClass())
136 )
137
138 val r1 =
139 factory.getOWLSubClassOfAxiom(
140 factory.getOWLObjectIntersectionOf(
141 (body.map(_._1) ++ head.map(_._2)): _*
142 ),
143 factory.getOWLNothing
144 )
145
146 val r2s =
147 for {
148 (a, na) <- head
149 hs = head.map(_._2).filterNot(_ equals na)
150 } yield factory.getOWLSubClassOfAxiom(
151 factory.getOWLObjectIntersectionOf(
152 (body.map(_._1) ++ hs): _*
153 ),
154 a
155 )
156
157 val r3s =
158 for {
159 (a, na) <- body
160 bs = body.map(_._1).filterNot(_ equals a)
161 } yield factory.getOWLSubClassOfAxiom(
162 factory.getOWLObjectIntersectionOf(
163 (bs ++ head.map(_._2)): _*
164 ),
165 na
166 )
167
168 List(r1) ++ r2s ++ r3s
169 }
170 case _ => List(axiom)
171 }
172 }
173 case _ => List(axiom)
174 }
175
176 /** Approximate a Horn-ALCHOIQ ontology to RSA
177 *
178 * This is done by gathering those axioms that prevent the ontology
179 * dependency graph from being tree-shaped, and removing them.
180 *
181 * @param axioms the set of axioms to approximate.
182 * @return the approximated set of axioms.
183 */
184 def toRSA(
185 axioms: List[OWLLogicalAxiom],
186 datafiles: List[File]
187 ): List[OWLLogicalAxiom] = {
188 /* Compute the dependency graph for the ontology */
189 val (graph, nodemap) = RSAUtil.dependencyGraph(axioms, datafiles)
190
191 /* Define node colors for the graph visit */
192 sealed trait NodeColor
193 case object Unvisited extends NodeColor
194 case object Visited extends NodeColor
195 case object ToDelete extends NodeColor
196
197 /* Keep track of node colors during graph visit */
198 var color = Map.from[Resource, NodeColor](
199 graph.nodes.toOuter.map(k => (k, Unvisited))
200 )
201
202 for {
203 component <- graph.componentTraverser().map(_ to Graph)
204 edge <- component
205 .outerEdgeTraverser(component.nodes.head)
206 .withKind(BreadthFirst)
207 } yield {
208 val source = edge._1
209 val target = edge._2
210 color(source) match {
211 case Unvisited | Visited => {
212 color(target) match {
213 case Unvisited =>
214 color(source) = Visited;
215 color(target) = Visited
216 case Visited =>
217 color(source) = ToDelete
218 case ToDelete =>
219 color(source) = Visited
220 }
221 }
222 case ToDelete =>
223 }
224 }
225
226 val toDelete = color.iterator.collect { case (resource: IRI, ToDelete) =>
227 nodemap(resource.getIRI)
228 }.toList
229
230 /* Remove axioms from approximated ontology */
231 axioms diff toDelete
232 }
233
234 // val edges1 = Seq('A ~> 'B, 'B ~> 'C, 'C ~> 'D, 'D ~> 'H, 'H ~>
235 // 'G, 'G ~> 'F, 'E ~> 'A, 'E ~> 'F, 'B ~> 'E, 'F ~> 'G, 'B ~> 'F,
236 // 'C ~> 'G, 'D ~> 'C, 'H ~> 'D)
237 // val edges2 = Seq('I ~> 'M, 'I ~> 'L, 'L ~> 'N, 'M ~> 'N)
238 // val edges3 = Seq('P ~> 'O)
239 // val graph = Graph.from(edges = edges1 ++ edges2 ++ edges3)
240 //
241}
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
4import org.semanticweb.owlapi.model._ 4import org.semanticweb.owlapi.model._
5 5
6import uk.ac.ox.cs.rsacomb.util.Logger 6import uk.ac.ox.cs.rsacomb.util.Logger
7import uk.ac.ox.cs.rsacomb.RSAOntology
7 8
8object Normalizer { 9object Normalizer {
9 10
@@ -25,12 +26,6 @@ class Normalizer() {
25 /** Simplify conversion between Java and Scala collections */ 26 /** Simplify conversion between Java and Scala collections */
26 import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ 27 import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._
27 28
28 private var counter = -1
29 def freshOWLClass(): OWLClass = {
30 counter += 1
31 factory.getOWLClass(s"X$counter")
32 }
33
34 /** Statistics */ 29 /** Statistics */
35 var discarded = 0 30 var discarded = 0
36 var shifted = 0 31 var shifted = 0
@@ -58,7 +53,7 @@ class Normalizer() {
58 * C c D -> { C c X, X c D } 53 * C c D -> { C c X, X c D }
59 */ 54 */
60 case _ if !sub.isOWLClass && !sup.isOWLClass => { 55 case _ if !sub.isOWLClass && !sup.isOWLClass => {
61 val cls = freshOWLClass() 56 val cls = RSAOntology.getFreshOWLClass()
62 Seq( 57 Seq(
63 factory.getOWLSubClassOfAxiom(sub, cls), 58 factory.getOWLSubClassOfAxiom(sub, cls),
64 factory.getOWLSubClassOfAxiom(cls, sup) 59 factory.getOWLSubClassOfAxiom(cls, sup)
@@ -79,7 +74,7 @@ class Normalizer() {
79 if (conj.isOWLClass) 74 if (conj.isOWLClass)
80 (acc1 :+ conj, acc2) 75 (acc1 :+ conj, acc2)
81 else { 76 else {
82 val cls = freshOWLClass() 77 val cls = RSAOntology.getFreshOWLClass()
83 ( 78 (
84 acc1 :+ cls, 79 acc1 :+ cls,
85 acc2 :+ factory.getOWLSubClassOfAxiom(conj, cls) 80 acc2 :+ factory.getOWLSubClassOfAxiom(conj, cls)
@@ -138,7 +133,7 @@ class Normalizer() {
138 */ 133 */
139 case (sub: OWLObjectSomeValuesFrom, _) 134 case (sub: OWLObjectSomeValuesFrom, _)
140 if !sub.getFiller.isOWLClass => { 135 if !sub.getFiller.isOWLClass => {
141 val cls = freshOWLClass() 136 val cls = RSAOntology.getFreshOWLClass()
142 Seq( 137 Seq(
143 factory.getOWLSubClassOfAxiom(sub.getFiller, cls), 138 factory.getOWLSubClassOfAxiom(sub.getFiller, cls),
144 factory.getOWLSubClassOfAxiom( 139 factory.getOWLSubClassOfAxiom(
@@ -153,7 +148,7 @@ class Normalizer() {
153 */ 148 */
154 case (_, sup: OWLObjectSomeValuesFrom) 149 case (_, sup: OWLObjectSomeValuesFrom)
155 if !sup.getFiller.isOWLClass => { 150 if !sup.getFiller.isOWLClass => {
156 val cls = freshOWLClass() 151 val cls = RSAOntology.getFreshOWLClass()
157 Seq( 152 Seq(
158 factory.getOWLSubClassOfAxiom(cls, sup.getFiller), 153 factory.getOWLSubClassOfAxiom(cls, sup.getFiller),
159 factory.getOWLSubClassOfAxiom( 154 factory.getOWLSubClassOfAxiom(
@@ -298,7 +293,7 @@ class Normalizer() {
298 ) 293 )
299 case (_, sup: OWLObjectMaxCardinality) 294 case (_, sup: OWLObjectMaxCardinality)
300 if sup.getCardinality == 1 && !sup.getFiller.isOWLClass => { 295 if sup.getCardinality == 1 && !sup.getFiller.isOWLClass => {
301 val cls = freshOWLClass() 296 val cls = RSAOntology.getFreshOWLClass()
302 Seq( 297 Seq(
303 factory.getOWLSubClassOfAxiom(cls, sup.getFiller), 298 factory.getOWLSubClassOfAxiom(cls, sup.getFiller),
304 factory.getOWLSubClassOfAxiom( 299 factory.getOWLSubClassOfAxiom(
@@ -488,7 +483,7 @@ class Normalizer() {
488 * C(a) -> { X(a), X c C } 483 * C(a) -> { X(a), X c C }
489 */ 484 */
490 case a: OWLClassAssertionAxiom if !a.getClassExpression.isOWLClass => { 485 case a: OWLClassAssertionAxiom if !a.getClassExpression.isOWLClass => {
491 val cls = freshOWLClass() 486 val cls = RSAOntology.getFreshOWLClass()
492 Seq( 487 Seq(
493 factory.getOWLClassAssertionAxiom(cls, a.getIndividual), 488 factory.getOWLClassAssertionAxiom(cls, a.getIndividual),
494 factory.getOWLSubClassOfAxiom(cls, a.getClassExpression) 489 factory.getOWLSubClassOfAxiom(cls, a.getClassExpression)
@@ -532,8 +527,10 @@ class Normalizer() {
532 sub: OWLClassExpression, 527 sub: OWLClassExpression,
533 sup: OWLObjectUnionOf 528 sup: OWLObjectUnionOf
534 ): Seq[OWLLogicalAxiom] = { 529 ): Seq[OWLLogicalAxiom] = {
535 val body = sub.asConjunctSet.map((atom) => (atom, freshOWLClass())) 530 val body =
536 val head = sup.asDisjunctSet.map((atom) => (atom, freshOWLClass())) 531 sub.asConjunctSet.map((atom) => (atom, RSAOntology.getFreshOWLClass()))
532 val head =
533 sup.asDisjunctSet.map((atom) => (atom, RSAOntology.getFreshOWLClass()))
537 534
538 /* Update statistics */ 535 /* Update statistics */
539 shifted += 1 536 shifted += 1