diff options
author | Federico Igne <federico.igne@cs.ox.ac.uk> | 2021-04-07 17:51:38 +0100 |
---|---|---|
committer | Federico Igne <federico.igne@cs.ox.ac.uk> | 2021-04-07 17:51:38 +0100 |
commit | 6f5c82982248e823f2dd6f9eaf87f552d1616ca4 (patch) | |
tree | e453115058e9b11c248607136107d8052649884b | |
parent | b622e151905d9a3b8fff14748cc840696dc85f16 (diff) | |
download | RSAComb-6f5c82982248e823f2dd6f9eaf87f552d1616ca4.tar.gz RSAComb-6f5c82982248e823f2dd6f9eaf87f552d1616ca4.zip |
Add approximation to RSA
Note that while the code is complete for approximating an input OWL2
ontology to RSA, the final steps are not "connected" yet, and the
approximation won't fire automatically. We still need to find a way to
include the approximation in the workflow in an efficient and
transparent way.
-rw-r--r-- | src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala | 266 |
1 files changed, 159 insertions, 107 deletions
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 763f4f5..2e055b9 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala | |||
@@ -21,7 +21,6 @@ import org.semanticweb.owlapi.model.{ | |||
21 | } | 21 | } |
22 | import org.semanticweb.owlapi.model.parameters.Imports | 22 | import org.semanticweb.owlapi.model.parameters.Imports |
23 | import org.semanticweb.owlapi.reasoner.structural.StructuralReasonerFactory | 23 | import org.semanticweb.owlapi.reasoner.structural.StructuralReasonerFactory |
24 | import org.semanticweb.owlapi.model.{IRI => OWLIRI} | ||
25 | import uk.ac.manchester.cs.owl.owlapi.OWLObjectPropertyImpl | 24 | import uk.ac.manchester.cs.owl.owlapi.OWLObjectPropertyImpl |
26 | 25 | ||
27 | import tech.oxfordsemantic.jrdfox.client.{ | 26 | import tech.oxfordsemantic.jrdfox.client.{ |
@@ -48,9 +47,10 @@ import tech.oxfordsemantic.jrdfox.logic.sparql.statement.SelectQuery | |||
48 | /* Scala imports */ | 47 | /* Scala imports */ |
49 | import scala.util.{Try, Success, Failure} | 48 | import scala.util.{Try, Success, Failure} |
50 | import scala.collection.JavaConverters._ | 49 | import scala.collection.JavaConverters._ |
51 | import scala.collection.mutable.Set | 50 | import scala.collection.mutable.{Set, Map} |
52 | import scalax.collection.immutable.Graph | 51 | import scalax.collection.Graph |
53 | import scalax.collection.GraphEdge.UnDiEdge | 52 | import scalax.collection.GraphPredef._, scalax.collection.GraphEdge._ |
53 | import scalax.collection.GraphTraversal._ | ||
54 | 54 | ||
55 | /* Debug only */ | 55 | /* Debug only */ |
56 | import org.semanticweb.owlapi.dlsyntax.renderer.DLSyntaxObjectRenderer | 56 | import org.semanticweb.owlapi.dlsyntax.renderer.DLSyntaxObjectRenderer |
@@ -170,103 +170,6 @@ class RSAOntology(_ontology: File, val datafiles: File*) { | |||
170 | .flatMap(_.objectPropertyExpressionsInSignature) | 170 | .flatMap(_.objectPropertyExpressionsInSignature) |
171 | .distinct | 171 | .distinct |
172 | 172 | ||
173 | /* Steps for RSA check | ||
174 | * 1) convert ontology axioms into LP rules | ||
175 | * 2) call RDFox on the onto and compute materialization | ||
176 | * 3) build graph from E(x,y) facts | ||
177 | * 4) check if the graph is tree-like | ||
178 | * ideally this annotates the graph with info about the reasons | ||
179 | * why the ontology might not be RSA. This could help a second | ||
180 | * step of approximation of an Horn-ALCHOIQ to RSA | ||
181 | * | ||
182 | * TODO: Implement additional checks (taking into account equality) | ||
183 | * | ||
184 | * To check if the graph is tree-like we check for acyclicity in a | ||
185 | * undirected graph. | ||
186 | */ | ||
187 | lazy val isRSA: Boolean = Logger.timed( | ||
188 | { | ||
189 | val unsafe = this.unsafeRoles | ||
190 | |||
191 | object RSAConverter extends RDFoxConverter { | ||
192 | |||
193 | override def convert( | ||
194 | expr: OWLClassExpression, | ||
195 | term: Term, | ||
196 | unsafe: List[OWLObjectPropertyExpression], | ||
197 | skolem: SkolemStrategy, | ||
198 | suffix: RSASuffix | ||
199 | ): Shards = | ||
200 | (expr, skolem) match { | ||
201 | |||
202 | case (e: OWLObjectSomeValuesFrom, c: Constant) | ||
203 | if unsafe contains e.getProperty => { | ||
204 | val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) | ||
205 | (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) | ||
206 | } | ||
207 | |||
208 | case (e: OWLDataSomeValuesFrom, c: Constant) | ||
209 | if unsafe contains e.getProperty => { | ||
210 | val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) | ||
211 | (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) | ||
212 | } | ||
213 | |||
214 | case _ => super.convert(expr, term, unsafe, skolem, suffix) | ||
215 | } | ||
216 | } | ||
217 | |||
218 | /* Ontology convertion into LP rules */ | ||
219 | val term = RSAOntology.genFreshVariable() | ||
220 | val conversion = Try( | ||
221 | axioms.map(a => | ||
222 | RSAConverter.convert(a, term, unsafe, new Constant(a), Empty) | ||
223 | ) | ||
224 | ) | ||
225 | |||
226 | conversion match { | ||
227 | case Success(result) => { | ||
228 | val datalog = result.unzip | ||
229 | val facts = datalog._1.flatten | ||
230 | var rules = datalog._2.flatten | ||
231 | |||
232 | /* Open connection with RDFox */ | ||
233 | val (server, data) = RDFoxUtil.openConnection("RSACheck") | ||
234 | |||
235 | /* Add additional built-in rules */ | ||
236 | val varX = Variable.create("X") | ||
237 | val varY = Variable.create("Y") | ||
238 | rules = Rule.create( | ||
239 | RSA.E(varX, varY), | ||
240 | RSA.PE(varX, varY), | ||
241 | RSA.U(varX), | ||
242 | RSA.U(varY) | ||
243 | ) :: rules | ||
244 | |||
245 | /* Load facts and rules from ontology */ | ||
246 | RDFoxUtil.addFacts(data, facts) | ||
247 | RDFoxUtil.addRules(data, rules) | ||
248 | /* Load data files */ | ||
249 | RDFoxUtil.addData(data, datafiles: _*) | ||
250 | |||
251 | /* Build graph */ | ||
252 | val graph = this.rsaGraph(data); | ||
253 | |||
254 | /* Close connection to RDFox */ | ||
255 | RDFoxUtil.closeConnection(server, data) | ||
256 | |||
257 | /* Acyclicity test */ | ||
258 | graph.isAcyclic | ||
259 | } | ||
260 | case Failure(e) => { | ||
261 | Logger print s"Unsupported axiom: $e" | ||
262 | false | ||
263 | } | ||
264 | } | ||
265 | }, | ||
266 | "RSA check", | ||
267 | Logger.DEBUG | ||
268 | ) | ||
269 | |||
270 | /** Unsafe roles of a given ontology. | 173 | /** Unsafe roles of a given ontology. |
271 | * | 174 | * |
272 | * Unsafety conditions are the following: | 175 | * Unsafety conditions are the following: |
@@ -321,16 +224,165 @@ class RSAOntology(_ontology: File, val datafiles: File*) { | |||
321 | (unsafe1 ++ unsafe2).toList | 224 | (unsafe1 ++ unsafe2).toList |
322 | } | 225 | } |
323 | 226 | ||
324 | private def rsaGraph( | 227 | /** RSA dependency graph |
325 | data: DataStoreConnection | 228 | * |
326 | ): Graph[Resource, UnDiEdge] = { | 229 | * This is used to check whether the input ontology is RSA. This also |
230 | * helps us determine a suitable approximation of the ontology to | ||
231 | * RSA. | ||
232 | */ | ||
233 | private lazy val dependencyGraph: Graph[Resource, DiEdge] = { | ||
234 | val unsafe = this.unsafeRoles | ||
235 | var nodemap = Map.empty[String, OWLAxiom] | ||
236 | |||
237 | object RSAConverter extends RDFoxConverter { | ||
238 | |||
239 | override def convert( | ||
240 | expr: OWLClassExpression, | ||
241 | term: Term, | ||
242 | unsafe: List[OWLObjectPropertyExpression], | ||
243 | skolem: SkolemStrategy, | ||
244 | suffix: RSASuffix | ||
245 | ): Shards = | ||
246 | (expr, skolem) match { | ||
247 | |||
248 | case (e: OWLObjectSomeValuesFrom, c: Constant) => { | ||
249 | nodemap.update(c.iri.getIRI, c.axiom) | ||
250 | val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) | ||
251 | if (unsafe contains e.getProperty) | ||
252 | (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) | ||
253 | else | ||
254 | (RSA.PE(term, c.iri) :: res, ext) | ||
255 | } | ||
256 | |||
257 | case (e: OWLDataSomeValuesFrom, c: Constant) => { | ||
258 | nodemap.update(c.iri.getIRI, c.axiom) | ||
259 | val (res, ext) = super.convert(e, term, unsafe, skolem, suffix) | ||
260 | if (unsafe contains e.getProperty) | ||
261 | (RSA.PE(term, c.iri) :: RSA.U(c.iri) :: res, ext) | ||
262 | else | ||
263 | (RSA.PE(term, c.iri) :: res, ext) | ||
264 | } | ||
265 | |||
266 | case _ => super.convert(expr, term, unsafe, skolem, suffix) | ||
267 | } | ||
268 | } | ||
269 | |||
270 | /* Ontology convertion into LP rules */ | ||
271 | val term = RSAOntology.genFreshVariable() | ||
272 | val result = axioms.map(a => | ||
273 | RSAConverter.convert(a, term, unsafe, new Constant(a), Empty) | ||
274 | ) | ||
275 | |||
276 | val datalog = result.unzip | ||
277 | val facts = datalog._1.flatten | ||
278 | var rules = datalog._2.flatten | ||
279 | |||
280 | /* Open connection with RDFox */ | ||
281 | val (server, data) = RDFoxUtil.openConnection("RSACheck") | ||
282 | |||
283 | /* Add additional built-in rules */ | ||
284 | val varX = Variable.create("X") | ||
285 | val varY = Variable.create("Y") | ||
286 | rules = Rule.create( | ||
287 | RSA.E(varX, varY), | ||
288 | RSA.PE(varX, varY), | ||
289 | RSA.U(varX), | ||
290 | RSA.U(varY) | ||
291 | ) :: rules | ||
292 | |||
293 | /* Load facts and rules from ontology */ | ||
294 | RDFoxUtil.addFacts(data, facts) | ||
295 | RDFoxUtil.addRules(data, rules) | ||
296 | /* Load data files */ | ||
297 | RDFoxUtil.addData(data, datafiles: _*) | ||
298 | |||
299 | /* Build graph */ | ||
327 | val query = "SELECT ?X ?Y WHERE { ?X rsa:E ?Y }" | 300 | val query = "SELECT ?X ?Y WHERE { ?X rsa:E ?Y }" |
328 | val answers = RDFoxUtil.submitQuery(data, query, RSA.Prefixes).get | 301 | val answers = RDFoxUtil.submitQuery(data, query, RSA.Prefixes).get |
329 | var edges: Seq[UnDiEdge[Resource]] = | 302 | var edges: Seq[DiEdge[Resource]] = |
330 | answers.collect { case (_, Seq(n1, n2)) => UnDiEdge(n1, n2) } | 303 | answers.collect { case (_, Seq(n1, n2)) => n1 ~> n2 } |
331 | Graph(edges: _*) | 304 | val graph = Graph(edges: _*) |
305 | |||
306 | /* Close connection to RDFox */ | ||
307 | RDFoxUtil.closeConnection(server, data) | ||
308 | |||
309 | /* Approximate the ontology to RSA */ | ||
310 | approximate(graph, nodemap) | ||
311 | |||
312 | graph | ||
332 | } | 313 | } |
333 | 314 | ||
315 | /** Approximate a Horn-ALCHOIQ ontology to RSA | ||
316 | * | ||
317 | * This is done by gathering those axioms that prevent the ontology | ||
318 | * dependency graph `dependencyGraph` from being tree-shaped, and | ||
319 | * removing them. | ||
320 | * | ||
321 | * @param graph the graph used to compute the axioms to remove. | ||
322 | * @param nodemap map from graph nodes to ontology axioms. | ||
323 | */ | ||
324 | private def approximate( | ||
325 | graph: Graph[Resource, DiEdge], | ||
326 | nodemap: Map[String, OWLAxiom] | ||
327 | ): Unit = { | ||
328 | |||
329 | /* Define node colors for the graph visit */ | ||
330 | sealed trait NodeColor | ||
331 | case object Unvisited extends NodeColor | ||
332 | case object Visited extends NodeColor | ||
333 | case object ToDelete extends NodeColor | ||
334 | |||
335 | /* Keep track of node colors during graph visit */ | ||
336 | var color = Map.from[Resource, NodeColor]( | ||
337 | graph.nodes.toOuter.map(k => (k, Unvisited)) | ||
338 | ) | ||
339 | |||
340 | for { | ||
341 | component <- graph.componentTraverser().map(_ to Graph) | ||
342 | edge <- component | ||
343 | .outerEdgeTraverser(component.nodes.head) | ||
344 | .withKind(BreadthFirst) | ||
345 | } yield { | ||
346 | val source = edge._1 | ||
347 | val target = edge._2 | ||
348 | color(source) match { | ||
349 | case Unvisited | Visited => { | ||
350 | color(target) match { | ||
351 | case Unvisited => | ||
352 | color(source) = Visited; | ||
353 | color(target) = Visited | ||
354 | case Visited => | ||
355 | color(source) = ToDelete | ||
356 | case ToDelete => | ||
357 | color(source) = Visited | ||
358 | } | ||
359 | } | ||
360 | case ToDelete => | ||
361 | } | ||
362 | } | ||
363 | |||
364 | val toDelete = color.iterator.collect { case (resource: IRI, ToDelete) => | ||
365 | nodemap(resource.getIRI) | ||
366 | }.toSeq | ||
367 | ontology.removeAxioms(toDelete: _*) | ||
368 | } | ||
369 | // val edges1 = Seq('A ~> 'B, 'B ~> 'C, 'C ~> 'D, 'D ~> 'H, 'H ~> | ||
370 | // 'G, 'G ~> 'F, 'E ~> 'A, 'E ~> 'F, 'B ~> 'E, 'F ~> 'G, 'B ~> 'F, | ||
371 | // 'C ~> 'G, 'D ~> 'C, 'H ~> 'D) | ||
372 | // val edges2 = Seq('I ~> 'M, 'I ~> 'L, 'L ~> 'N, 'M ~> 'N) | ||
373 | // val edges3 = Seq('P ~> 'O) | ||
374 | // val graph = Graph.from(edges = edges1 ++ edges2 ++ edges3) | ||
375 | |||
376 | /** RSA check | ||
377 | * | ||
378 | * Acyclicity check over *undirected* dependency graph. | ||
379 | * NOTE: at the moment we are using the direct version of the graph. | ||
380 | * | ||
381 | * @deprecated | ||
382 | */ | ||
383 | lazy val isRSA: Boolean = | ||
384 | Logger.timed(dependencyGraph.isAcyclic, "RSA check", Logger.DEBUG) | ||
385 | |||
334 | /** Top axiomatization rules | 386 | /** Top axiomatization rules |
335 | * | 387 | * |
336 | * For each concept/role *in the ontology file* introduce a rule to | 388 | * For each concept/role *in the ontology file* introduce a rule to |