diff options
4 files changed, 681 insertions, 2 deletions
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 3777c6b..3da6c8a 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/CanonicalModel.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/CanonicalModel.scala | |||
@@ -79,7 +79,9 @@ class CanonicalModel(val ontology: RSAOntology) { | |||
79 | val term = RSAOntology.genFreshVariable() | 79 | val term = RSAOntology.genFreshVariable() |
80 | val unsafe = ontology.unsafeRoles | 80 | val unsafe = ontology.unsafeRoles |
81 | ontology.axioms | 81 | ontology.axioms |
82 | .map(CanonicalModelConverter.convert(_, term, unsafe, NoSkolem, Empty)) | 82 | .map(a => |
83 | CanonicalModelConverter.convert(a, term, unsafe, Constant(a), Empty) | ||
84 | ) | ||
83 | .unzip | 85 | .unzip |
84 | } | 86 | } |
85 | ( | 87 | ( |
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 0f1552a..42a5b87 100644 --- a/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/RSAOntology.scala | |||
@@ -99,6 +99,8 @@ class RSAOntology(_ontology: File, val datafiles: File*) { | |||
99 | private val reasoner = | 99 | private val reasoner = |
100 | (new StructuralReasonerFactory()).createReasoner(ontology) | 100 | (new StructuralReasonerFactory()).createReasoner(ontology) |
101 | 101 | ||
102 | private val normalizer = new Normalizer() | ||
103 | |||
102 | /** Imported knowledge base. */ | 104 | /** Imported knowledge base. */ |
103 | //lazy val kbase: OWLOntology = { | 105 | //lazy val kbase: OWLOntology = { |
104 | // val merger = new OWLOntologyMerger(manager) | 106 | // val merger = new OWLOntologyMerger(manager) |
@@ -112,6 +114,7 @@ class RSAOntology(_ontology: File, val datafiles: File*) { | |||
112 | .tboxAxioms(Imports.INCLUDED) | 114 | .tboxAxioms(Imports.INCLUDED) |
113 | .collect(Collectors.toList()) | 115 | .collect(Collectors.toList()) |
114 | .collect { case a: OWLLogicalAxiom => a } | 116 | .collect { case a: OWLLogicalAxiom => a } |
117 | .flatMap(normalizer.normalize) | ||
115 | Logger.print(s"Original TBox: ${tbox.length}", Logger.DEBUG) | 118 | Logger.print(s"Original TBox: ${tbox.length}", Logger.DEBUG) |
116 | 119 | ||
117 | /** RBox axioms */ | 120 | /** RBox axioms */ |
@@ -120,6 +123,7 @@ class RSAOntology(_ontology: File, val datafiles: File*) { | |||
120 | .rboxAxioms(Imports.INCLUDED) | 123 | .rboxAxioms(Imports.INCLUDED) |
121 | .collect(Collectors.toList()) | 124 | .collect(Collectors.toList()) |
122 | .collect { case a: OWLLogicalAxiom => a } | 125 | .collect { case a: OWLLogicalAxiom => a } |
126 | .flatMap(normalizer.normalize) | ||
123 | Logger.print(s"Original RBox: ${rbox.length}", Logger.DEBUG) | 127 | Logger.print(s"Original RBox: ${rbox.length}", Logger.DEBUG) |
124 | 128 | ||
125 | /** ABox axioms | 129 | /** ABox axioms |
@@ -134,7 +138,8 @@ class RSAOntology(_ontology: File, val datafiles: File*) { | |||
134 | .aboxAxioms(Imports.INCLUDED) | 138 | .aboxAxioms(Imports.INCLUDED) |
135 | .collect(Collectors.toList()) | 139 | .collect(Collectors.toList()) |
136 | .collect { case a: OWLLogicalAxiom => a } | 140 | .collect { case a: OWLLogicalAxiom => a } |
137 | Logger.print(s"Original RBox: ${abox.length}", Logger.DEBUG) | 141 | .flatMap(normalizer.normalize) |
142 | Logger.print(s"Original ABox: ${abox.length}", Logger.DEBUG) | ||
138 | 143 | ||
139 | /** Collection of logical axioms in the input ontology */ | 144 | /** Collection of logical axioms in the input ontology */ |
140 | lazy val axioms: List[OWLLogicalAxiom] = abox ::: tbox ::: rbox | 145 | lazy val axioms: List[OWLLogicalAxiom] = abox ::: tbox ::: rbox |
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 new file mode 100644 index 0000000..3c4dd5c --- /dev/null +++ b/src/main/scala/uk/ac/ox/cs/rsacomb/converter/Normalizer.scala | |||
@@ -0,0 +1,532 @@ | |||
1 | package uk.ac.ox.cs.rsacomb.converter | ||
2 | |||
3 | import org.semanticweb.owlapi.apibinding.OWLManager | ||
4 | import org.semanticweb.owlapi.model._ | ||
5 | |||
6 | import uk.ac.ox.cs.rsacomb.util.Logger | ||
7 | |||
8 | object Normalizer { | ||
9 | |||
10 | /** Factory used for converting expressions when needed. | ||
11 | * | ||
12 | * @note most of the time this is used to perform some kind of | ||
13 | * normalization on axioms. In later versions it might be worth | ||
14 | * moving the normalization code in its own independent step. | ||
15 | */ | ||
16 | private val manager = OWLManager.createOWLOntologyManager() | ||
17 | val factory = manager.getOWLDataFactory() | ||
18 | |||
19 | } | ||
20 | |||
21 | class Normalizer() { | ||
22 | |||
23 | import Normalizer._ | ||
24 | |||
25 | /** Simplify conversion between Java and Scala collections */ | ||
26 | import uk.ac.ox.cs.rsacomb.implicits.JavaCollections._ | ||
27 | |||
28 | private var counter = -1 | ||
29 | def freshOWLClass(): OWLClass = factory.getOWLClass(s"X${counter += 1}") | ||
30 | |||
31 | /** Normalizes a | ||
32 | * [[org.semanticweb.owlapi.model.OWLLogicalAxiom OWLLogicalAxiom]] | ||
33 | * | ||
34 | * @note not all possible axioms are supported. Following is a list | ||
35 | * of all unhandled class expressions: | ||
36 | * - [[org.semanticweb.owlapi.model.OWLAsymmetricObjectPropertyAxiom OWLAsymmetricObjectPropertyAxiom]] | ||
37 | * - [[org.semanticweb.owlapi.model.OWLDatatypeDefinitionAxiom OWLDatatypeDefinitionAxiom]] | ||
38 | * - [[org.semanticweb.owlapi.model.OWLDisjointDataPropertiesAxiom OWLDisjointDataPropertiesAxiom]] | ||
39 | * - [[org.semanticweb.owlapi.model.OWLDisjointObjectPropertiesAxiom OWLDisjointObjectPropertiesAxiom]] | ||
40 | * - [[org.semanticweb.owlapi.model.OWLHasKeyAxiom OWLHasKeyAxiom]] | ||
41 | * - [[org.semanticweb.owlapi.model.SWRLRule SWRLRule]] | ||
42 | */ | ||
43 | def normalize(axiom: OWLLogicalAxiom): Seq[OWLLogicalAxiom] = | ||
44 | axiom match { | ||
45 | case a: OWLSubClassOfAxiom => { | ||
46 | val sub = a.getSubClass.getNNF | ||
47 | val sup = a.getSuperClass.getNNF | ||
48 | (sub, sup) match { | ||
49 | /** Split complex subclass axioms | ||
50 | * | ||
51 | * C c D -> { C c X, X c D } | ||
52 | */ | ||
53 | case _ if !sub.isOWLClass && !sup.isOWLClass => { | ||
54 | val cls = freshOWLClass() | ||
55 | Seq( | ||
56 | factory.getOWLSubClassOfAxiom(sub, cls), | ||
57 | factory.getOWLSubClassOfAxiom(cls, sup) | ||
58 | ).flatMap(normalize) | ||
59 | } | ||
60 | /** Conjunction on the lhs | ||
61 | * | ||
62 | * A1 n ... n C n ... n An c D -> { C c X, A1 n ... n X n ... n An c D } | ||
63 | */ | ||
64 | case (sub: OWLObjectIntersectionOf, _) | ||
65 | if sub.asConjunctSet.exists(c => !c.isOWLClass) => { | ||
66 | var additional = Seq() | ||
67 | val conjuncts = sub.asConjunctSet | ||
68 | if (conjuncts.length > 0) { | ||
69 | val acc = (Seq[OWLClassExpression](), Seq[OWLLogicalAxiom]()) | ||
70 | val (acc1, acc2) = conjuncts.foldLeft(acc)( | ||
71 | { case ((acc1, acc2), conj) => | ||
72 | if (conj.isOWLClass) | ||
73 | (acc1 :+ conj, acc2) | ||
74 | else { | ||
75 | val cls = freshOWLClass() | ||
76 | ( | ||
77 | acc1 :+ cls, | ||
78 | acc2 :+ factory.getOWLSubClassOfAxiom(conj, cls) | ||
79 | ) | ||
80 | } | ||
81 | } | ||
82 | ) | ||
83 | (acc2 :+ factory.getOWLSubClassOfAxiom( | ||
84 | factory.getOWLObjectIntersectionOf(acc1: _*), | ||
85 | sup | ||
86 | )) | ||
87 | .flatMap(normalize) | ||
88 | } else { | ||
89 | normalize(factory.getOWLSubClassOfAxiom(factory.getOWLThing, sup)) | ||
90 | } | ||
91 | } | ||
92 | /** Conjunction on the rhs | ||
93 | * | ||
94 | * D c A1 n ... n An -> { D c A1, ... , D c An } | ||
95 | */ | ||
96 | case (_, sup: OWLObjectIntersectionOf) => { | ||
97 | val conjuncts = sup.asConjunctSet | ||
98 | if (conjuncts.length > 0) { | ||
99 | conjuncts | ||
100 | .map(cls => factory.getOWLSubClassOfAxiom(sub, cls)) | ||
101 | .flatMap(normalize) | ||
102 | } else { | ||
103 | normalize(factory.getOWLSubClassOfAxiom(sub, factory.getOWLThing)) | ||
104 | } | ||
105 | } | ||
106 | /** Disjunction on the lhs | ||
107 | * | ||
108 | * A1 u ... u An c D -> { A1 c D, ... , An c D } | ||
109 | */ | ||
110 | case (sub: OWLObjectUnionOf, _) => { | ||
111 | val disjuncts = sub.asDisjunctSet | ||
112 | if (disjuncts.length > 0) { | ||
113 | disjuncts | ||
114 | .map(cls => factory.getOWLSubClassOfAxiom(cls, sup)) | ||
115 | .flatMap(normalize) | ||
116 | } else { | ||
117 | normalize( | ||
118 | factory.getOWLSubClassOfAxiom(factory.getOWLNothing, sup) | ||
119 | ) | ||
120 | } | ||
121 | } | ||
122 | /** Disjunction on the rhs is not supported */ | ||
123 | case (_, sup: OWLObjectUnionOf) => notInHornALCHOIQ(a) | ||
124 | /** Complex class expression on existential restriction on the lhs | ||
125 | * | ||
126 | * exists R . C c D -> { C c X, exists R . X c D } | ||
127 | */ | ||
128 | case (sub: OWLObjectSomeValuesFrom, _) | ||
129 | if !sub.getFiller.isOWLClass => { | ||
130 | val cls = freshOWLClass() | ||
131 | Seq( | ||
132 | factory.getOWLSubClassOfAxiom(sub.getFiller, cls), | ||
133 | factory.getOWLSubClassOfAxiom( | ||
134 | factory.getOWLObjectSomeValuesFrom(sub.getProperty, cls), | ||
135 | sup | ||
136 | ) | ||
137 | ).flatMap(normalize) | ||
138 | } | ||
139 | /** Complex class expression on existential restriction on the rhs | ||
140 | * | ||
141 | * C c exists R . D -> { X c D, C c exists R . X } | ||
142 | */ | ||
143 | case (_, sup: OWLObjectSomeValuesFrom) | ||
144 | if !sup.getFiller.isOWLClass => { | ||
145 | val cls = freshOWLClass() | ||
146 | Seq( | ||
147 | factory.getOWLSubClassOfAxiom(cls, sup.getFiller), | ||
148 | factory.getOWLSubClassOfAxiom( | ||
149 | sub, | ||
150 | factory.getOWLObjectSomeValuesFrom(sup.getProperty, cls) | ||
151 | ) | ||
152 | ).flatMap(normalize) | ||
153 | } | ||
154 | /** Object/Data universal quantification on the lhs not supported */ | ||
155 | case (sub: OWLObjectAllValuesFrom, _) => notInHornALCHOIQ(a) | ||
156 | case (sub: OWLDataAllValuesFrom, _) => notInHornALCHOIQ(a) | ||
157 | /** Object universal quantification on the rhs | ||
158 | * | ||
159 | * C c forall R . D -> exists R- . C c D | ||
160 | */ | ||
161 | case (_, sup: OWLObjectAllValuesFrom) => | ||
162 | normalize( | ||
163 | factory.getOWLSubClassOfAxiom( | ||
164 | factory | ||
165 | .getOWLObjectSomeValuesFrom( | ||
166 | sup.getProperty.getInverseProperty, | ||
167 | sub | ||
168 | ), | ||
169 | sup.getFiller | ||
170 | ) | ||
171 | ) | ||
172 | /** Object universal quantification on the rhs not supported */ | ||
173 | case (_, sup: OWLDataAllValuesFrom) => notInHornALCHOIQ(a) | ||
174 | /** Exact object/data cardinality restriction on the lhs/rhs | ||
175 | * | ||
176 | * = i R . C -> <= i R . C n >= i R . X | ||
177 | */ | ||
178 | case (sub: OWLObjectExactCardinality, _) => | ||
179 | normalize( | ||
180 | factory.getOWLSubClassOfAxiom(sub.asIntersectionOfMinMax, sup) | ||
181 | ) | ||
182 | case (sub: OWLDataExactCardinality, _) => | ||
183 | normalize( | ||
184 | factory.getOWLSubClassOfAxiom(sub.asIntersectionOfMinMax, sup) | ||
185 | ) | ||
186 | case (_, sup: OWLObjectExactCardinality) => | ||
187 | normalize( | ||
188 | factory.getOWLSubClassOfAxiom(sub, sup.asIntersectionOfMinMax) | ||
189 | ) | ||
190 | case (_, sup: OWLDataExactCardinality) => | ||
191 | normalize( | ||
192 | factory.getOWLSubClassOfAxiom(sub, sup.asIntersectionOfMinMax) | ||
193 | ) | ||
194 | /** Min object/data cardinality restriction on the lhs/rhs | ||
195 | * | ||
196 | * >= 0 R . C -> top | ||
197 | * >= 1 R . C -> exists R . C | ||
198 | * | ||
199 | * @note not supported when restriction >= 2. | ||
200 | */ | ||
201 | case (sub: OWLObjectMinCardinality, _) => | ||
202 | sub.getCardinality match { | ||
203 | case 0 => | ||
204 | normalize( | ||
205 | factory.getOWLSubClassOfAxiom(factory.getOWLThing, sup) | ||
206 | ) | ||
207 | case 1 => | ||
208 | normalize( | ||
209 | factory.getOWLSubClassOfAxiom( | ||
210 | factory.getOWLObjectSomeValuesFrom( | ||
211 | sub.getProperty, | ||
212 | sub.getFiller | ||
213 | ), | ||
214 | sup | ||
215 | ) | ||
216 | ) | ||
217 | case _ => notInHornALCHOIQ(a) | ||
218 | } | ||
219 | case (sub: OWLDataMinCardinality, _) => | ||
220 | sub.getCardinality match { | ||
221 | case 0 => | ||
222 | normalize( | ||
223 | factory.getOWLSubClassOfAxiom(factory.getOWLThing, sup) | ||
224 | ) | ||
225 | case 1 => | ||
226 | normalize( | ||
227 | factory.getOWLSubClassOfAxiom( | ||
228 | factory.getOWLDataSomeValuesFrom( | ||
229 | sub.getProperty, | ||
230 | sub.getFiller | ||
231 | ), | ||
232 | sup | ||
233 | ) | ||
234 | ) | ||
235 | case _ => notInHornALCHOIQ(a) | ||
236 | } | ||
237 | case (_, sup: OWLObjectMinCardinality) => | ||
238 | sup.getCardinality match { | ||
239 | case 0 => Seq() | ||
240 | case 1 => | ||
241 | normalize( | ||
242 | factory.getOWLSubClassOfAxiom( | ||
243 | sub, | ||
244 | factory.getOWLObjectSomeValuesFrom( | ||
245 | sup.getProperty, | ||
246 | sup.getFiller | ||
247 | ) | ||
248 | ) | ||
249 | ) | ||
250 | case _ => notInHornALCHOIQ(a) | ||
251 | } | ||
252 | case (_, sup: OWLDataMinCardinality) => | ||
253 | sup.getCardinality match { | ||
254 | case 0 => Seq() | ||
255 | case 1 => | ||
256 | normalize( | ||
257 | factory.getOWLSubClassOfAxiom( | ||
258 | sub, | ||
259 | factory.getOWLDataSomeValuesFrom( | ||
260 | sup.getProperty, | ||
261 | sup.getFiller | ||
262 | ) | ||
263 | ) | ||
264 | ) | ||
265 | case _ => notInHornALCHOIQ(a) | ||
266 | } | ||
267 | /** Max object/data cardinality restriction on the lhs not supported */ | ||
268 | case (sub: OWLObjectMaxCardinality, _) => notInHornALCHOIQ(a) | ||
269 | case (sub: OWLDataMaxCardinality, _) => notInHornALCHOIQ(a) | ||
270 | /** Max object/data cardinality restriction on the rhs | ||
271 | * | ||
272 | * C c <= 0 R . D -> C n exists R . D -> bot | ||
273 | * C c <= 1 R . D -> { X c D, C c <= 1 R . D } | ||
274 | * | ||
275 | * @note not supported when restriction >= 2. | ||
276 | */ | ||
277 | case (_, sup: OWLObjectMaxCardinality) if sup.getCardinality == 0 => | ||
278 | normalize( | ||
279 | factory.getOWLSubClassOfAxiom( | ||
280 | factory.getOWLObjectIntersectionOf( | ||
281 | sub, | ||
282 | factory | ||
283 | .getOWLObjectSomeValuesFrom(sup.getProperty, sup.getFiller) | ||
284 | ), | ||
285 | factory.getOWLNothing | ||
286 | ) | ||
287 | ) | ||
288 | case (_, sup: OWLObjectMaxCardinality) | ||
289 | if sup.getCardinality == 1 && !sup.getFiller.isOWLClass => { | ||
290 | val cls = freshOWLClass() | ||
291 | Seq( | ||
292 | factory.getOWLSubClassOfAxiom(cls, sup.getFiller), | ||
293 | factory.getOWLSubClassOfAxiom( | ||
294 | sub, | ||
295 | factory.getOWLObjectMaxCardinality(1, sup.getProperty, cls) | ||
296 | ) | ||
297 | ).flatMap(normalize) | ||
298 | } | ||
299 | case (_, sup: OWLObjectMaxCardinality) if sup.getCardinality >= 2 => | ||
300 | notInHornALCHOIQ(a) | ||
301 | case (_, sup: OWLDataMaxCardinality) if sup.getCardinality == 0 => | ||
302 | normalize( | ||
303 | factory.getOWLSubClassOfAxiom( | ||
304 | factory.getOWLObjectIntersectionOf( | ||
305 | sub, | ||
306 | factory | ||
307 | .getOWLDataSomeValuesFrom(sup.getProperty, sup.getFiller) | ||
308 | ), | ||
309 | factory.getOWLNothing | ||
310 | ) | ||
311 | ) | ||
312 | case (_, sup: OWLDataMaxCardinality) if sup.getCardinality >= 1 => | ||
313 | notInHornALCHOIQ(a) | ||
314 | /** HasValue expression on the lhs/rhs | ||
315 | * | ||
316 | * HasValue(R, a) -> exists R . {a} | ||
317 | */ | ||
318 | case (sub: OWLObjectHasValue, _) => | ||
319 | normalize( | ||
320 | factory.getOWLSubClassOfAxiom( | ||
321 | factory.getOWLObjectSomeValuesFrom( | ||
322 | sub.getProperty, | ||
323 | factory.getOWLObjectOneOf(sub.getFiller) | ||
324 | ), | ||
325 | sup | ||
326 | ) | ||
327 | ) | ||
328 | case (sub: OWLDataHasValue, _) => | ||
329 | normalize( | ||
330 | factory.getOWLSubClassOfAxiom( | ||
331 | factory.getOWLDataSomeValuesFrom( | ||
332 | sub.getProperty, | ||
333 | factory.getOWLDataOneOf(sub.getFiller) | ||
334 | ), | ||
335 | sup | ||
336 | ) | ||
337 | ) | ||
338 | case (_, sup: OWLObjectHasValue) => | ||
339 | normalize( | ||
340 | factory.getOWLSubClassOfAxiom( | ||
341 | sub, | ||
342 | factory.getOWLObjectSomeValuesFrom( | ||
343 | sup.getProperty, | ||
344 | factory.getOWLObjectOneOf(sup.getFiller) | ||
345 | ) | ||
346 | ) | ||
347 | ) | ||
348 | case (_, sup: OWLDataHasValue) => | ||
349 | normalize( | ||
350 | factory.getOWLSubClassOfAxiom( | ||
351 | sub, | ||
352 | factory.getOWLDataSomeValuesFrom( | ||
353 | sup.getProperty, | ||
354 | factory.getOWLDataOneOf(sup.getFiller) | ||
355 | ) | ||
356 | ) | ||
357 | ) | ||
358 | /** Enumeration of individuals on the lhs | ||
359 | * | ||
360 | * {a1, ... ,an} c D -> { D(a1), ..., D(an) } | ||
361 | */ | ||
362 | case (sub: OWLObjectOneOf, _) => | ||
363 | sub.getIndividuals.map(factory.getOWLClassAssertionAxiom(sup, _)) | ||
364 | /** Enumeration of individuals on the rhs | ||
365 | * It's supported only when of cardinality < 2. | ||
366 | */ | ||
367 | case (_, sup: OWLObjectOneOf) if sup.getIndividuals.length == 0 => | ||
368 | normalize(factory.getOWLSubClassOfAxiom(sub, factory.getOWLNothing)) | ||
369 | case (_, sup: OWLObjectOneOf) if sup.getIndividuals.length > 2 => | ||
370 | notInHornALCHOIQ(a) | ||
371 | /** Class complement on the lhs | ||
372 | * | ||
373 | * ~C c D -> top c C n D | ||
374 | */ | ||
375 | case (sub: OWLObjectComplementOf, _) => | ||
376 | normalize( | ||
377 | factory.getOWLSubClassOfAxiom( | ||
378 | factory.getOWLThing, | ||
379 | factory.getOWLObjectIntersectionOf(sub.getComplementNNF, sup) | ||
380 | ) | ||
381 | ) | ||
382 | /** Class complement on the rhs | ||
383 | * | ||
384 | * C c ~D -> C n D c bot | ||
385 | */ | ||
386 | case (_, sup: OWLObjectComplementOf) => | ||
387 | normalize( | ||
388 | factory.getOWLSubClassOfAxiom( | ||
389 | factory.getOWLObjectIntersectionOf(sup.getComplementNNF, sub), | ||
390 | factory.getOWLNothing | ||
391 | ) | ||
392 | ) | ||
393 | /** Axiom is already normalized */ | ||
394 | case _ => Seq(a) | ||
395 | } | ||
396 | } | ||
397 | |||
398 | case a: OWLEquivalentClassesAxiom => { | ||
399 | a.getAxiomWithoutAnnotations.asOWLSubClassOfAxioms.flatMap(normalize) | ||
400 | } | ||
401 | |||
402 | case a: OWLEquivalentObjectPropertiesAxiom => { | ||
403 | a.getAxiomWithoutAnnotations.asSubObjectPropertyOfAxioms.flatMap( | ||
404 | normalize | ||
405 | ) | ||
406 | } | ||
407 | |||
408 | case a: OWLEquivalentDataPropertiesAxiom => { | ||
409 | a.getAxiomWithoutAnnotations.asSubDataPropertyOfAxioms.flatMap( | ||
410 | normalize | ||
411 | ) | ||
412 | } | ||
413 | |||
414 | case a: OWLObjectPropertyDomainAxiom => | ||
415 | normalize(a.getAxiomWithoutAnnotations.asOWLSubClassOfAxiom) | ||
416 | |||
417 | case a: OWLObjectPropertyRangeAxiom => | ||
418 | normalize(a.getAxiomWithoutAnnotations.asOWLSubClassOfAxiom) | ||
419 | |||
420 | case a: OWLDataPropertyDomainAxiom => | ||
421 | normalize(a.getAxiomWithoutAnnotations.asOWLSubClassOfAxiom) | ||
422 | |||
423 | case a: OWLDataPropertyRangeAxiom => | ||
424 | normalize(a.getAxiomWithoutAnnotations.asOWLSubClassOfAxiom) | ||
425 | |||
426 | case a: OWLDisjointClassesAxiom => | ||
427 | a.asPairwiseAxioms.map((a) => { | ||
428 | val classes = a.getAxiomWithoutAnnotations.getClassExpressions | ||
429 | factory.getOWLSubClassOfAxiom( | ||
430 | factory.getOWLObjectIntersectionOf(classes), | ||
431 | factory.getOWLNothing | ||
432 | ) | ||
433 | }) | ||
434 | |||
435 | case a: OWLInverseObjectPropertiesAxiom => | ||
436 | a.getAxiomWithoutAnnotations.asSubObjectPropertyOfAxioms.flatMap( | ||
437 | normalize | ||
438 | ) | ||
439 | |||
440 | case a: OWLFunctionalObjectPropertyAxiom => | ||
441 | normalize(a.getAxiomWithoutAnnotations.asOWLSubClassOfAxiom) | ||
442 | |||
443 | case a: OWLFunctionalDataPropertyAxiom => | ||
444 | normalize(a.getAxiomWithoutAnnotations.asOWLSubClassOfAxiom) | ||
445 | |||
446 | case a: OWLInverseFunctionalObjectPropertyAxiom => | ||
447 | normalize(a.getAxiomWithoutAnnotations.asOWLSubClassOfAxiom) | ||
448 | |||
449 | case a: OWLSymmetricObjectPropertyAxiom => | ||
450 | a.getAxiomWithoutAnnotations.asSubPropertyAxioms.flatMap(normalize) | ||
451 | |||
452 | case a: OWLDifferentIndividualsAxiom => | ||
453 | a.asPairwiseAxioms.map((a) => { | ||
454 | val classes = a.getIndividuals.map(factory.getOWLObjectOneOf(_)) | ||
455 | factory.getOWLSubClassOfAxiom( | ||
456 | factory.getOWLObjectIntersectionOf(classes), | ||
457 | factory.getOWLNothing | ||
458 | ) | ||
459 | }) | ||
460 | |||
461 | case a: OWLIrreflexiveObjectPropertyAxiom => | ||
462 | normalize(a.getAxiomWithoutAnnotations.asOWLSubClassOfAxiom) | ||
463 | |||
464 | case a: OWLSameIndividualAxiom => | ||
465 | a.getAxiomWithoutAnnotations.asOWLSubClassOfAxioms.flatMap(normalize) | ||
466 | |||
467 | case a: OWLDisjointUnionAxiom => | ||
468 | Seq(a.getOWLDisjointClassesAxiom, a.getOWLEquivalentClassesAxiom) | ||
469 | .flatMap(normalize) | ||
470 | |||
471 | /** Complex class assertion | ||
472 | * | ||
473 | * C(a) -> { X(a), X c C } | ||
474 | */ | ||
475 | case a: OWLClassAssertionAxiom if !a.getClassExpression.isOWLClass => { | ||
476 | val cls = freshOWLClass() | ||
477 | Seq( | ||
478 | factory.getOWLClassAssertionAxiom(cls, a.getIndividual), | ||
479 | factory.getOWLSubClassOfAxiom(cls, a.getClassExpression) | ||
480 | ).flatMap(normalize) | ||
481 | } | ||
482 | |||
483 | case a: OWLNegativeObjectPropertyAssertionAxiom => | ||
484 | normalize(a.getAxiomWithoutAnnotations.asOWLSubClassOfAxiom) | ||
485 | |||
486 | case a: OWLNegativeDataPropertyAssertionAxiom => | ||
487 | normalize(a.getAxiomWithoutAnnotations.asOWLSubClassOfAxiom) | ||
488 | |||
489 | /** Not in Horn-ALCHOIQ */ | ||
490 | |||
491 | case a: OWLTransitiveObjectPropertyAxiom => notInHornALCHOIQ(a) | ||
492 | |||
493 | case a: OWLReflexiveObjectPropertyAxiom => notInHornALCHOIQ(a) | ||
494 | |||
495 | case a: OWLSubPropertyChainOfAxiom => notInHornALCHOIQ(a) | ||
496 | |||
497 | /** Unsupported */ | ||
498 | |||
499 | case a: OWLAsymmetricObjectPropertyAxiom => notSupported(a) | ||
500 | |||
501 | case a: OWLDatatypeDefinitionAxiom => notSupported(a) | ||
502 | |||
503 | case a: OWLDisjointDataPropertiesAxiom => notSupported(a) | ||
504 | |||
505 | case a: OWLDisjointObjectPropertiesAxiom => notSupported(a) | ||
506 | |||
507 | case a: OWLHasKeyAxiom => notSupported(a) | ||
508 | |||
509 | case a: SWRLRule => notSupported(a) | ||
510 | |||
511 | /** Axiom is already normalized */ | ||
512 | case a => Seq(a) | ||
513 | } | ||
514 | |||
515 | /** Approximation function for axioms out of Horn-ALCHOIQ | ||
516 | * | ||
517 | * By default discards the axiom, which guarantees a lower bound | ||
518 | * ontology w.r.t. CQ answering. | ||
519 | */ | ||
520 | protected def notInHornALCHOIQ( | ||
521 | axiom: OWLLogicalAxiom | ||
522 | ): Seq[OWLLogicalAxiom] = { | ||
523 | Logger print s"'$axiom' has been ignored because it is not in Horn-ALCHOIQ" | ||
524 | Seq() | ||
525 | } | ||
526 | |||
527 | /** Non supported axioms */ | ||
528 | private def notSupported(axiom: OWLLogicalAxiom): Seq[OWLLogicalAxiom] = | ||
529 | throw new RuntimeException( | ||
530 | s"'$axiom' is not currently supported." | ||
531 | ) | ||
532 | } | ||
diff --git a/src/test/scala/uk/ac/ox/cs/rsacomb/converter/NormalizerSpec.scala b/src/test/scala/uk/ac/ox/cs/rsacomb/converter/NormalizerSpec.scala new file mode 100644 index 0000000..3a686c0 --- /dev/null +++ b/src/test/scala/uk/ac/ox/cs/rsacomb/converter/NormalizerSpec.scala | |||
@@ -0,0 +1,140 @@ | |||
1 | package uk.ac.ox.cs.rsacomb.converter | ||
2 | |||
3 | import org.scalatest.LoneElement | ||
4 | import org.scalatest.flatspec.AnyFlatSpec | ||
5 | import org.scalatest.matchers.should.Matchers | ||
6 | import org.semanticweb.owlapi.apibinding.OWLManager | ||
7 | import org.semanticweb.owlapi.model.OWLOntologyManager | ||
8 | |||
9 | import tech.oxfordsemantic.jrdfox.logic.datalog.TupleTableAtom | ||
10 | import tech.oxfordsemantic.jrdfox.logic.expression.{Variable, IRI} | ||
11 | import uk.ac.ox.cs.rsacomb.converter.RDFoxConverter | ||
12 | import uk.ac.ox.cs.rsacomb.suffix.{Empty, Forward, Backward, Inverse} | ||
13 | import uk.ac.ox.cs.rsacomb.converter.{Normalizer, SkolemStrategy, NoSkolem} | ||
14 | |||
15 | object NormalizerSpec { | ||
16 | val manager = OWLManager.createOWLOntologyManager() | ||
17 | val factory = manager.getOWLDataFactory | ||
18 | val normalizer = new Normalizer() | ||
19 | } | ||
20 | |||
21 | class NormalizerSpec extends AnyFlatSpec with Matchers with LoneElement { | ||
22 | |||
23 | import NormalizerSpec._ | ||
24 | |||
25 | "Equivalent classes" should "be split in pairwise subclass axioms" in { | ||
26 | val cls1 = factory.getOWLClass("_:cls1") | ||
27 | val cls2 = factory.getOWLClass("_:cls2") | ||
28 | val cls3 = factory.getOWLClass("_:cls3") | ||
29 | val equivalentClasses = | ||
30 | factory.getOWLEquivalentClassesAxiom(cls1, cls2, cls3) | ||
31 | normalizer.normalize(equivalentClasses) should contain theSameElementsAs | ||
32 | Seq( | ||
33 | factory.getOWLSubClassOfAxiom(cls1, cls2), | ||
34 | factory.getOWLSubClassOfAxiom(cls1, cls3), | ||
35 | factory.getOWLSubClassOfAxiom(cls2, cls1), | ||
36 | factory.getOWLSubClassOfAxiom(cls2, cls3), | ||
37 | factory.getOWLSubClassOfAxiom(cls3, cls1), | ||
38 | factory.getOWLSubClassOfAxiom(cls3, cls2) | ||
39 | ).flatMap(normalizer.normalize) | ||
40 | } | ||
41 | |||
42 | "Equivalent data properties" should "be split in pairwise subclass axioms" in { | ||
43 | val prop1 = factory.getOWLDataProperty("_:prop1") | ||
44 | val prop2 = factory.getOWLDataProperty("_:prop2") | ||
45 | val prop3 = factory.getOWLDataProperty("_:prop3") | ||
46 | val equivalentProps = | ||
47 | factory.getOWLEquivalentDataPropertiesAxiom(prop1, prop2, prop3) | ||
48 | normalizer.normalize(equivalentProps) should contain theSameElementsAs | ||
49 | Seq( | ||
50 | factory.getOWLSubDataPropertyOfAxiom(prop1, prop2), | ||
51 | factory.getOWLSubDataPropertyOfAxiom(prop1, prop3), | ||
52 | factory.getOWLSubDataPropertyOfAxiom(prop2, prop1), | ||
53 | factory.getOWLSubDataPropertyOfAxiom(prop2, prop3), | ||
54 | factory.getOWLSubDataPropertyOfAxiom(prop3, prop1), | ||
55 | factory.getOWLSubDataPropertyOfAxiom(prop3, prop2) | ||
56 | ).flatMap(normalizer.normalize) | ||
57 | } | ||
58 | |||
59 | "Equivalent object properties" should "be split in pairwise subclass axioms" in { | ||
60 | val prop1 = factory.getOWLObjectProperty("_:prop1") | ||
61 | val prop2 = factory.getOWLObjectProperty("_:prop2") | ||
62 | val prop3 = factory.getOWLObjectProperty("_:prop3") | ||
63 | val equivalentProps = | ||
64 | factory.getOWLEquivalentObjectPropertiesAxiom(prop1, prop2, prop3) | ||
65 | normalizer.normalize(equivalentProps) should contain theSameElementsAs | ||
66 | Seq( | ||
67 | factory.getOWLSubObjectPropertyOfAxiom(prop1, prop2), | ||
68 | factory.getOWLSubObjectPropertyOfAxiom(prop1, prop3), | ||
69 | factory.getOWLSubObjectPropertyOfAxiom(prop2, prop1), | ||
70 | factory.getOWLSubObjectPropertyOfAxiom(prop2, prop3), | ||
71 | factory.getOWLSubObjectPropertyOfAxiom(prop3, prop1), | ||
72 | factory.getOWLSubObjectPropertyOfAxiom(prop3, prop2) | ||
73 | ).flatMap(normalizer.normalize) | ||
74 | } | ||
75 | |||
76 | //"A class name" should "be converted into a single atom" in { | ||
77 | // val cls = factory.getOWLClass(iriString0) | ||
78 | // val atom = TupleTableAtom.rdf(term0, IRI.RDF_TYPE, IRI.create(iriString0)) | ||
79 | // val (res, ext) = | ||
80 | // convert(cls, term0, List(), NoSkolem, Empty) | ||
81 | // res.loneElement shouldEqual atom | ||
82 | // ext shouldBe empty | ||
83 | //} | ||
84 | |||
85 | //"A intersection of classes" should "be converted into the union of the conversion of the classes" in { | ||
86 | // val cls0 = factory.getOWLClass(iriString0) | ||
87 | // val cls1 = factory.getOWLClass(iriString1) | ||
88 | // val cls2 = factory.getOWLClass(iriString2) | ||
89 | // val conj = factory.getOWLObjectIntersectionOf(cls0, cls1, cls2) | ||
90 | // val (res0, ext0) = | ||
91 | // convert(cls0, term0, List(), NoSkolem, Empty) | ||
92 | // val (res1, ext1) = | ||
93 | // convert(cls1, term0, List(), NoSkolem, Empty) | ||
94 | // val (res2, ext2) = | ||
95 | // convert(cls2, term0, List(), NoSkolem, Empty) | ||
96 | // val (res, ext) = | ||
97 | // convert(conj, term0, List(), NoSkolem, Empty) | ||
98 | // res should contain theSameElementsAs (res0 ::: res1 ::: res2) | ||
99 | // ext should contain theSameElementsAs (ext0 ::: ext1 ::: ext2) | ||
100 | //} | ||
101 | |||
102 | //"A singleton intersection" should "correspond to the conversion of the internal class" in { | ||
103 | // val cls0 = factory.getOWLClass(iriString0) | ||
104 | // val conj = factory.getOWLObjectIntersectionOf(cls0) | ||
105 | // val (res0, ext0) = | ||
106 | // convert(cls0, term0, List(), NoSkolem, Empty) | ||
107 | // val (res, ext) = | ||
108 | // convert(conj, term0, List(), NoSkolem, Empty) | ||
109 | // res should contain theSameElementsAs res0 | ||
110 | // ext should contain theSameElementsAs ext0 | ||
111 | //} | ||
112 | |||
113 | //"An object property" should "be converted into an atom with matching predicate" in { | ||
114 | // val prop = factory.getOWLObjectProperty(iriString0) | ||
115 | // for (sx <- suffixes) { | ||
116 | // val atom = | ||
117 | // TupleTableAtom.rdf(term0, IRI.create(iriString0 :: sx), term1) | ||
118 | // convert(prop, term0, term1, sx) shouldEqual atom | ||
119 | // } | ||
120 | //} | ||
121 | |||
122 | //"The inverse of an object property" should "be converted into an atom with inverted subject/object" in { | ||
123 | // val prop = factory.getOWLObjectProperty(iriString0) | ||
124 | // val inv = factory.getOWLObjectInverseOf(prop) | ||
125 | // for (sx <- Seq(Empty, Forward, Backward)) { | ||
126 | // val atom = TupleTableAtom.rdf(term1, IRI.create(iriString0 :: sx), term0) | ||
127 | // convert(inv, term0, term1, sx) shouldEqual atom | ||
128 | // } | ||
129 | //} | ||
130 | |||
131 | //"A data property" should "be converted into an atom with matching predicate" in { | ||
132 | // val prop = factory.getOWLDataProperty(iriString0) | ||
133 | // for (suffix <- suffixes) { | ||
134 | // val atom = | ||
135 | // TupleTableAtom.rdf(term0, IRI.create(iriString0 :: suffix), term1) | ||
136 | // convert(prop, term0, term1, suffix) shouldEqual atom | ||
137 | // } | ||
138 | //} | ||
139 | |||
140 | } | ||