diff options
Diffstat (limited to 'src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala')
-rw-r--r-- | src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala | 380 |
1 files changed, 380 insertions, 0 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 new file mode 100644 index 0000000..ac86e3d --- /dev/null +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala | |||
@@ -0,0 +1,380 @@ | |||
1 | package uk.ac.ox.cs.rsacomb | ||
2 | |||
3 | /* Java imports */ | ||
4 | import java.util.HashMap | ||
5 | import java.util.stream.{Collectors, Stream} | ||
6 | import java.io.File | ||
7 | import org.semanticweb.owlapi.apibinding.OWLManager | ||
8 | |||
9 | import org.semanticweb.owlapi.model.{OWLOntology, OWLAxiom} | ||
10 | import org.semanticweb.owlapi.model.{ | ||
11 | OWLClass, | ||
12 | OWLObjectProperty, | ||
13 | OWLSubObjectPropertyOfAxiom, | ||
14 | OWLObjectPropertyExpression, | ||
15 | OWLObjectSomeValuesFrom, | ||
16 | OWLSubClassOfAxiom | ||
17 | } | ||
18 | import org.semanticweb.owlapi.model.parameters.Imports | ||
19 | import org.semanticweb.owlapi.reasoner.structural.StructuralReasonerFactory | ||
20 | import org.semanticweb.owlapi.model.{IRI => OWLIRI} | ||
21 | import uk.ac.manchester.cs.owl.owlapi.OWLObjectPropertyImpl | ||
22 | |||
23 | import tech.oxfordsemantic.jrdfox.client.{UpdateType, DataStoreConnection} | ||
24 | import tech.oxfordsemantic.jrdfox.logic.datalog.{ | ||
25 | Rule, | ||
26 | TupleTableAtom, | ||
27 | Negation, | ||
28 | BodyFormula | ||
29 | } | ||
30 | import tech.oxfordsemantic.jrdfox.logic.expression.{ | ||
31 | Term, | ||
32 | Variable, | ||
33 | IRI, | ||
34 | Resource | ||
35 | } | ||
36 | import tech.oxfordsemantic.jrdfox.logic.sparql.statement.SelectQuery | ||
37 | |||
38 | /* Scala imports */ | ||
39 | import scala.collection.JavaConverters._ | ||
40 | import scala.collection.mutable.Set | ||
41 | import scalax.collection.immutable.Graph | ||
42 | import scalax.collection.GraphEdge.UnDiEdge | ||
43 | |||
44 | /* Debug only */ | ||
45 | import org.semanticweb.owlapi.dlsyntax.renderer.DLSyntaxObjectRenderer | ||
46 | import tech.oxfordsemantic.jrdfox.logic._ | ||
47 | import org.semanticweb.owlapi.model.OWLObjectInverseOf | ||
48 | |||
49 | import uk.ac.ox.cs.rsacomb.converter.{RDFoxAxiomConverter, SkolemStrategy} | ||
50 | import uk.ac.ox.cs.rsacomb.implicits.RSAAxiom | ||
51 | import uk.ac.ox.cs.rsacomb.suffix.{Empty, Forward, Backward, Inverse} | ||
52 | import uk.ac.ox.cs.rsacomb.util.{RDFoxHelpers, RSA} | ||
53 | |||
54 | object RSAOntology { | ||
55 | |||
56 | // Counter used to implement a simple fresh variable generator | ||
57 | private var counter = -1; | ||
58 | |||
59 | def apply(ontology: OWLOntology): RSAOntology = new RSAOntology(ontology) | ||
60 | |||
61 | def apply(ontology: File): RSAOntology = | ||
62 | new RSAOntology(loadOntology(ontology)) | ||
63 | |||
64 | def genFreshVariable(): Variable = { | ||
65 | counter += 1 | ||
66 | Variable.create(f"I$counter%03d") | ||
67 | } | ||
68 | |||
69 | private def loadOntology(onto: File): OWLOntology = { | ||
70 | val manager = OWLManager.createOWLOntologyManager() | ||
71 | manager.loadOntologyFromOntologyDocument(onto) | ||
72 | } | ||
73 | } | ||
74 | |||
75 | class RSAOntology(val ontology: OWLOntology) extends RSAAxiom { | ||
76 | |||
77 | // Gather TBox/RBox/ABox from original ontology | ||
78 | val tbox: List[OWLAxiom] = | ||
79 | ontology | ||
80 | .tboxAxioms(Imports.INCLUDED) | ||
81 | .collect(Collectors.toList()) | ||
82 | .asScala | ||
83 | .toList | ||
84 | |||
85 | val rbox: List[OWLAxiom] = | ||
86 | ontology | ||
87 | .rboxAxioms(Imports.INCLUDED) | ||
88 | .collect(Collectors.toList()) | ||
89 | .asScala | ||
90 | .toList | ||
91 | |||
92 | val abox: List[OWLAxiom] = | ||
93 | ontology | ||
94 | .aboxAxioms(Imports.INCLUDED) | ||
95 | .collect(Collectors.toList()) | ||
96 | .asScala | ||
97 | .toList | ||
98 | |||
99 | val axioms: List[OWLAxiom] = abox ::: tbox ::: rbox | ||
100 | |||
101 | /* Retrieve individuals in the original ontology | ||
102 | */ | ||
103 | val individuals: List[IRI] = | ||
104 | ontology | ||
105 | .getIndividualsInSignature() | ||
106 | .asScala | ||
107 | .map(_.getIRI) | ||
108 | .map(implicits.RDFox.owlapiToRdfoxIri) | ||
109 | .toList | ||
110 | |||
111 | val concepts: List[OWLClass] = | ||
112 | ontology.getClassesInSignature().asScala.toList | ||
113 | |||
114 | val roles: List[OWLObjectPropertyExpression] = | ||
115 | axioms | ||
116 | .flatMap(_.objectPropertyExpressionsInSignature) | ||
117 | .distinct | ||
118 | |||
119 | // OWLAPI reasoner for same easier tasks | ||
120 | private val reasoner = | ||
121 | (new StructuralReasonerFactory()).createReasoner(ontology) | ||
122 | |||
123 | /* Steps for RSA check | ||
124 | * 1) convert ontology axioms into LP rules | ||
125 | * 2) call RDFox on the onto and compute materialization | ||
126 | * 3) build graph from E(x,y) facts | ||
127 | * 4) check if the graph is tree-like | ||
128 | * ideally this annotates the graph with info about the reasons | ||
129 | * why the ontology might not be RSA. This could help a second | ||
130 | * step of approximation of an Horn-ALCHOIQ to RSA | ||
131 | */ | ||
132 | lazy val isRSA: Boolean = { | ||
133 | |||
134 | val unsafe = this.unsafeRoles | ||
135 | |||
136 | /* DEBUG: print rules in DL syntax and unsafe roles */ | ||
137 | //val renderer = new DLSyntaxObjectRenderer() | ||
138 | //println("\nDL rules:") | ||
139 | //axioms.foreach(x => println(renderer.render(x))) | ||
140 | //println("\nUnsafe roles:") | ||
141 | //println(unsafe) | ||
142 | |||
143 | /* Ontology convertion into LP rules */ | ||
144 | val datalog = for { | ||
145 | axiom <- axioms | ||
146 | visitor = new RDFoxAxiomConverter( | ||
147 | RSAOntology.genFreshVariable(), | ||
148 | unsafe, | ||
149 | SkolemStrategy.ConstantRSA(axiom.toString), | ||
150 | Empty | ||
151 | ) | ||
152 | rule <- axiom.accept(visitor) | ||
153 | } yield rule | ||
154 | |||
155 | /* DEBUG: print datalog rules */ | ||
156 | //println("\nDatalog roles:") | ||
157 | //datalog.foreach(println) | ||
158 | |||
159 | // Open connection with RDFox | ||
160 | val (server, data) = RDFoxHelpers.openConnection("RSACheck") | ||
161 | // Add Data (hardcoded for now) | ||
162 | //data.importData(UpdateType.ADDITION, RSA.Prefixes, ":a a :A .") | ||
163 | |||
164 | /* Add built-in rules | ||
165 | */ | ||
166 | data.importData( | ||
167 | UpdateType.ADDITION, | ||
168 | RSA.Prefixes, | ||
169 | "rsa:E[?X,?Y] :- rsa:PE[?X,?Y], rsa:U[?X], rsa:U[?Y] ." | ||
170 | ) | ||
171 | |||
172 | /* Add built-in rules | ||
173 | */ | ||
174 | // data.importData( | ||
175 | // UpdateType.ADDITION, | ||
176 | // RSA.Prefixes, | ||
177 | // "[?entity, a, ?superClass] :- [?entity, a, ?class], [?class, rdfs:subClassOf, ?superClass] ." | ||
178 | // ) | ||
179 | |||
180 | /* Add ontology rules | ||
181 | */ | ||
182 | data.addRules(datalog.asJava) | ||
183 | |||
184 | /* Build graph | ||
185 | */ | ||
186 | val graph = this.rsaGraph(data); | ||
187 | //println(graph) | ||
188 | |||
189 | // Close connection to RDFox | ||
190 | RDFoxHelpers.closeConnection(server, data) | ||
191 | |||
192 | /* To check if the graph is tree-like we check for acyclicity in a | ||
193 | * undirected graph. | ||
194 | * | ||
195 | * TODO: Implement additional checks (taking into account equality) | ||
196 | */ | ||
197 | graph.isAcyclic | ||
198 | } | ||
199 | |||
200 | lazy val unsafeRoles: List[OWLObjectPropertyExpression] = { | ||
201 | |||
202 | /* DEBUG: print rules in DL syntax */ | ||
203 | //val renderer = new DLSyntaxObjectRenderer() | ||
204 | |||
205 | /* Checking for (1) unsafety condition: | ||
206 | * | ||
207 | * For all roles r1 appearing in an axiom of type T5, r1 is unsafe | ||
208 | * if there exists a role r2 (different from top) appearing in an axiom | ||
209 | * of type T3 and r1 is a subproperty of the inverse of r2. | ||
210 | */ | ||
211 | val unsafe1 = for { | ||
212 | axiom <- tbox | ||
213 | if axiom.isT5 | ||
214 | role1 <- axiom.objectPropertyExpressionsInSignature | ||
215 | roleSuper = | ||
216 | role1 +: reasoner | ||
217 | .superObjectProperties(role1) | ||
218 | .collect(Collectors.toList()) | ||
219 | .asScala | ||
220 | roleSuperInv = roleSuper.map(_.getInverseProperty) | ||
221 | axiom <- tbox | ||
222 | if axiom.isT3 && !axiom.isT3top | ||
223 | role2 <- axiom.objectPropertyExpressionsInSignature | ||
224 | if roleSuperInv.contains(role2) | ||
225 | } yield role1 | ||
226 | |||
227 | /* Checking for (2) unsafety condition: | ||
228 | * | ||
229 | * For all roles p1 appearing in an axiom of type T5, p1 is unsafe if | ||
230 | * there exists a role p2 appearing in an axiom of type T4 and p1 is a | ||
231 | * subproperty of either p2 or the inverse of p2. | ||
232 | * | ||
233 | */ | ||
234 | val unsafe2 = for { | ||
235 | axiom <- tbox | ||
236 | if axiom.isT5 | ||
237 | role1 <- axiom.objectPropertyExpressionsInSignature | ||
238 | roleSuper = | ||
239 | role1 +: reasoner | ||
240 | .superObjectProperties(role1) | ||
241 | .collect(Collectors.toList()) | ||
242 | .asScala | ||
243 | roleSuperInv = roleSuper.map(_.getInverseProperty) | ||
244 | axiom <- tbox | ||
245 | if axiom.isT4 | ||
246 | role2 <- axiom.objectPropertyExpressionsInSignature | ||
247 | if roleSuper.contains(role2) || roleSuperInv.contains(role2) | ||
248 | } yield role1 | ||
249 | |||
250 | (unsafe1 ++ unsafe2).toList | ||
251 | } | ||
252 | |||
253 | private def rsaGraph( | ||
254 | data: DataStoreConnection | ||
255 | ): Graph[Resource, UnDiEdge] = { | ||
256 | val query = "SELECT ?X ?Y WHERE { ?X rsa:E ?Y }" | ||
257 | val answers = RDFoxHelpers.submitSelectQuery(data, query, RSA.Prefixes) | ||
258 | var edges: List[UnDiEdge[Resource]] = answers.map { | ||
259 | case n1 :: n2 :: _ => UnDiEdge(n1, n2) | ||
260 | } | ||
261 | Graph(edges: _*) | ||
262 | } | ||
263 | |||
264 | def filteringProgram( | ||
265 | query: SelectQuery, | ||
266 | nis: List[Term] | ||
267 | ): FilteringProgram = | ||
268 | new FilteringProgram(query, nis) | ||
269 | |||
270 | lazy val canonicalModel = new CanonicalModel(this) | ||
271 | |||
272 | // TODO: the following functions needs testing | ||
273 | def confl( | ||
274 | role: OWLObjectPropertyExpression | ||
275 | ): Set[OWLObjectPropertyExpression] = { | ||
276 | |||
277 | val invSuperRoles = reasoner | ||
278 | .superObjectProperties(role) | ||
279 | .collect(Collectors.toSet()) | ||
280 | .asScala | ||
281 | .addOne(role) | ||
282 | .map(_.getInverseProperty) | ||
283 | |||
284 | invSuperRoles | ||
285 | .flatMap(x => | ||
286 | reasoner | ||
287 | .subObjectProperties(x) | ||
288 | .collect(Collectors.toSet()) | ||
289 | .asScala | ||
290 | .addOne(x) | ||
291 | ) | ||
292 | .filterNot(_.isOWLBottomObjectProperty()) | ||
293 | .filterNot(_.getInverseProperty.isOWLTopObjectProperty()) | ||
294 | } | ||
295 | |||
296 | def self(axiom: OWLSubClassOfAxiom): Set[Term] = { | ||
297 | // Assuming just one role in the signature of a T5 axiom | ||
298 | val role = axiom.objectPropertyExpressionsInSignature(0) | ||
299 | if (this.confl(role).contains(role)) { | ||
300 | Set( | ||
301 | RSA("v0_" ++ axiom.hashed), | ||
302 | RSA("v1_" ++ axiom.hashed) | ||
303 | ) | ||
304 | } else { | ||
305 | Set() | ||
306 | } | ||
307 | } | ||
308 | |||
309 | // def cycle(axiom: OWLSubClassOfAxiom): Set[Term] = { | ||
310 | // // Assuming just one role in the signature of a T5 axiom | ||
311 | // val roleR = axiom.objectPropertyExpressionsInSignature(0) | ||
312 | // val conflR = this.confl(roleR) | ||
313 | // // We just need the TBox to find | ||
314 | // val tbox = ontology | ||
315 | // .tboxAxioms(Imports.INCLUDED) | ||
316 | // .collect(Collectors.toSet()) | ||
317 | // .asScala | ||
318 | // for { | ||
319 | // axiom1 <- tbox | ||
320 | // // TODO: is this an optimization or an error? | ||
321 | // if axiom1.isT5 | ||
322 | // // We expect only one role coming out of a T5 axiom | ||
323 | // roleS <- axiom1.objectPropertyExpressionsInSignature | ||
324 | // // Triples ordering is among triples involving safe roles. | ||
325 | // if !unsafeRoles.contains(roleS) | ||
326 | // if conflR.contains(roleS) | ||
327 | // individual = | ||
328 | // if (axiom.hashCode < axiom1.hashCode) { | ||
329 | // RSA.rsa("v0_" ++ axiom1.hashCode.toString()) | ||
330 | // } else { | ||
331 | // RSA.rsa("v1_" ++ axiom1.hashCode.toString()) | ||
332 | // } | ||
333 | // } yield individual | ||
334 | // } | ||
335 | |||
336 | def cycle(axiom: OWLSubClassOfAxiom): Set[Term] = { | ||
337 | // TODO: we can actually use `toTriple` from `RSAAxiom` | ||
338 | val classes = | ||
339 | axiom.classesInSignature.collect(Collectors.toList()).asScala | ||
340 | val classA = classes(0) | ||
341 | val roleR = axiom | ||
342 | .objectPropertyExpressionsInSignature(0) | ||
343 | .asInstanceOf[OWLObjectProperty] | ||
344 | val classB = classes(1) | ||
345 | cycle_aux(classA, roleR, classB) | ||
346 | } | ||
347 | |||
348 | def cycle_aux( | ||
349 | classA: OWLClass, | ||
350 | roleR: OWLObjectProperty, | ||
351 | classB: OWLClass | ||
352 | ): Set[Term] = { | ||
353 | val conflR = this.confl(roleR) | ||
354 | val classes = ontology | ||
355 | .classesInSignature(Imports.INCLUDED) | ||
356 | .collect(Collectors.toSet()) | ||
357 | .asScala | ||
358 | for { | ||
359 | classD <- classes | ||
360 | roleS <- conflR | ||
361 | classC <- classes | ||
362 | // Keeping this check for now | ||
363 | if !unsafeRoles.contains(roleS) | ||
364 | tripleARB = RSAAxiom.hashed(classA, roleR, classB) | ||
365 | tripleDSC = RSAAxiom.hashed(classD, roleS, classC) | ||
366 | individual = | ||
367 | if (tripleARB > tripleDSC) { | ||
368 | RSA("v1_" ++ tripleDSC) | ||
369 | } else { | ||
370 | // Note that this is also the case for | ||
371 | // `tripleARB == tripleDSC` | ||
372 | RSA("v0_" ++ tripleDSC) | ||
373 | } | ||
374 | } yield individual | ||
375 | } | ||
376 | |||
377 | def unfold(axiom: OWLSubClassOfAxiom): Set[Term] = | ||
378 | this.self(axiom) | this.cycle(axiom) | ||
379 | |||
380 | } // implicit class RSAOntology | ||