1use crate::ConceptPool;
14use crate::ir::{ConceptExpr, ConceptId};
15use crate::ontology::{Axiom, InternalOntology};
16
17#[must_use]
41pub fn to_nnf(cid: ConceptId, pool: &mut ConceptPool) -> ConceptId {
42 let expr = pool.get(cid).clone();
43 match expr {
44 ConceptExpr::Top
45 | ConceptExpr::Bot
46 | ConceptExpr::Atomic(_)
47 | ConceptExpr::Nominal(_)
48 | ConceptExpr::SelfRestriction(_) => cid,
49 ConceptExpr::Not(inner) => push_negation_in(inner, pool),
50 ConceptExpr::And(args) => {
51 let normalized: Vec<ConceptId> = args.iter().map(|&c| to_nnf(c, pool)).collect();
52 pool.and(normalized)
53 }
54 ConceptExpr::Or(args) => {
55 let normalized: Vec<ConceptId> = args.iter().map(|&c| to_nnf(c, pool)).collect();
56 pool.or(normalized)
57 }
58 ConceptExpr::Some(role, c) => {
59 let c_nnf = to_nnf(c, pool);
60 pool.some(role, c_nnf)
61 }
62 ConceptExpr::All(role, c) => {
63 let c_nnf = to_nnf(c, pool);
64 pool.all(role, c_nnf)
65 }
66 ConceptExpr::Min(n, role, c) => {
67 let c_nnf = to_nnf(c, pool);
68 pool.min(n, role, c_nnf)
69 }
70 ConceptExpr::Max(n, role, c) => {
71 let c_nnf = to_nnf(c, pool);
72 pool.max(n, role, c_nnf)
73 }
74 }
75}
76
77#[must_use]
81pub fn nnf_complement(cid: ConceptId, pool: &mut ConceptPool) -> ConceptId {
82 push_negation_in(cid, pool)
83}
84
85fn push_negation_in(cid: ConceptId, pool: &mut ConceptPool) -> ConceptId {
87 let expr = pool.get(cid).clone();
88 match expr {
89 ConceptExpr::Top => pool.bot(),
90 ConceptExpr::Bot => pool.top(),
91 ConceptExpr::Atomic(_) | ConceptExpr::Nominal(_) | ConceptExpr::SelfRestriction(_) => {
92 pool.not(cid)
93 }
94 ConceptExpr::Not(inner) => to_nnf(inner, pool),
96 ConceptExpr::And(args) => {
98 let negated: Vec<ConceptId> = args.iter().map(|&c| push_negation_in(c, pool)).collect();
99 pool.or(negated)
100 }
101 ConceptExpr::Or(args) => {
103 let negated: Vec<ConceptId> = args.iter().map(|&c| push_negation_in(c, pool)).collect();
104 pool.and(negated)
105 }
106 ConceptExpr::Some(role, c) => {
108 let c_neg = push_negation_in(c, pool);
109 pool.all(role, c_neg)
110 }
111 ConceptExpr::All(role, c) => {
113 let c_neg = push_negation_in(c, pool);
114 pool.some(role, c_neg)
115 }
116 ConceptExpr::Min(n, role, c) => {
119 if n == 0 {
120 pool.bot()
121 } else {
122 let c_nnf = to_nnf(c, pool);
123 pool.max(n - 1, role, c_nnf)
124 }
125 }
126 ConceptExpr::Max(n, role, c) => {
128 let c_nnf = to_nnf(c, pool);
129 pool.min(n + 1, role, c_nnf)
130 }
131 }
132}
133
134#[must_use]
137pub fn is_nnf(cid: ConceptId, pool: &ConceptPool) -> bool {
138 match pool.get(cid) {
139 ConceptExpr::Top
140 | ConceptExpr::Bot
141 | ConceptExpr::Atomic(_)
142 | ConceptExpr::Nominal(_)
143 | ConceptExpr::SelfRestriction(_) => true,
144 ConceptExpr::Not(inner) => matches!(
145 pool.get(*inner),
146 ConceptExpr::Atomic(_) | ConceptExpr::Nominal(_) | ConceptExpr::SelfRestriction(_)
147 ),
148 ConceptExpr::And(args) | ConceptExpr::Or(args) => args.iter().all(|&c| is_nnf(c, pool)),
149 ConceptExpr::Some(_, c)
150 | ConceptExpr::All(_, c)
151 | ConceptExpr::Min(_, _, c)
152 | ConceptExpr::Max(_, _, c) => is_nnf(*c, pool),
153 }
154}
155
156pub fn nnf_axioms(ontology: &mut InternalOntology) -> Vec<Axiom> {
164 let axioms = &ontology.axioms;
166 let pool = &mut ontology.concepts;
167 let mut out = Vec::with_capacity(axioms.len());
168 for ax in axioms {
169 out.push(nnf_axiom(ax, pool));
170 }
171 out
172}
173
174fn nnf_axiom(ax: &Axiom, pool: &mut ConceptPool) -> Axiom {
178 match ax {
179 Axiom::SubClassOf { sub, sup } => Axiom::SubClassOf {
180 sub: to_nnf(*sub, pool),
181 sup: to_nnf(*sup, pool),
182 },
183 Axiom::EquivalentClasses(ids) => {
184 Axiom::EquivalentClasses(ids.iter().map(|&c| to_nnf(c, pool)).collect())
185 }
186 Axiom::DisjointClasses(ids) => {
187 Axiom::DisjointClasses(ids.iter().map(|&c| to_nnf(c, pool)).collect())
188 }
189 Axiom::DisjointUnion { class, members } => Axiom::DisjointUnion {
190 class: *class,
191 members: members.iter().map(|&c| to_nnf(c, pool)).collect(),
192 },
193 Axiom::ObjectPropertyDomain { role, domain } => Axiom::ObjectPropertyDomain {
194 role: *role,
195 domain: to_nnf(*domain, pool),
196 },
197 Axiom::ObjectPropertyRange { role, range } => Axiom::ObjectPropertyRange {
198 role: *role,
199 range: to_nnf(*range, pool),
200 },
201 Axiom::ClassAssertion { class, individual } => Axiom::ClassAssertion {
202 class: to_nnf(*class, pool),
203 individual: *individual,
204 },
205 other => other.clone(),
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use crate::ir::{ClassId, IndividualId, Role, RoleId};
215
216 fn pool() -> ConceptPool {
217 ConceptPool::new()
218 }
219
220 #[test]
221 fn nnf_of_atomic_is_identity() {
222 let mut p = pool();
223 let a = p.atomic(ClassId::new(0));
224 assert_eq!(to_nnf(a, &mut p), a);
225 }
226
227 #[test]
228 fn nnf_of_not_top_is_bot() {
229 let mut p = pool();
230 let t = p.top();
231 let not_top = p.not(t);
232 let nnf = to_nnf(not_top, &mut p);
233 assert_eq!(nnf, p.bot());
234 }
235
236 #[test]
237 fn nnf_of_not_bot_is_top() {
238 let mut p = pool();
239 let b = p.bot();
240 let not_bot = p.not(b);
241 let nnf = to_nnf(not_bot, &mut p);
242 assert_eq!(nnf, p.top());
243 }
244
245 #[test]
246 fn nnf_of_double_negation_is_inner() {
247 let mut p = pool();
248 let a = p.atomic(ClassId::new(0));
249 let not_a = p.not(a);
250 let not_not_a = p.not(not_a);
251 assert_eq!(to_nnf(not_not_a, &mut p), a);
252 }
253
254 #[test]
255 fn nnf_of_not_atomic_keeps_one_not() {
256 let mut p = pool();
257 let a = p.atomic(ClassId::new(0));
258 let not_a = p.not(a);
259 assert_eq!(to_nnf(not_a, &mut p), not_a);
260 }
261
262 #[test]
263 fn nnf_pushes_through_and_via_de_morgan() {
264 let mut p = pool();
266 let a = p.atomic(ClassId::new(0));
267 let b = p.atomic(ClassId::new(1));
268 let and_ab = p.and([a, b]);
269 let not_and = p.not(and_ab);
270 let result = to_nnf(not_and, &mut p);
271 let na = p.not(a);
272 let nb = p.not(b);
273 let expected = p.or([na, nb]);
274 assert_eq!(result, expected);
275 }
276
277 #[test]
278 fn nnf_pushes_through_or_via_de_morgan() {
279 let mut p = pool();
281 let a = p.atomic(ClassId::new(0));
282 let b = p.atomic(ClassId::new(1));
283 let or_ab = p.or([a, b]);
284 let not_or = p.not(or_ab);
285 let result = to_nnf(not_or, &mut p);
286 let na = p.not(a);
287 let nb = p.not(b);
288 let expected = p.and([na, nb]);
289 assert_eq!(result, expected);
290 }
291
292 #[test]
293 fn nnf_pushes_through_some_to_all_with_negated_inner() {
294 let mut p = pool();
296 let r = Role::named(RoleId::new(0));
297 let a = p.atomic(ClassId::new(0));
298 let some_a = p.some(r, a);
299 let not_some = p.not(some_a);
300 let result = to_nnf(not_some, &mut p);
301 let na = p.not(a);
302 let expected = p.all(r, na);
303 assert_eq!(result, expected);
304 }
305
306 #[test]
307 fn nnf_pushes_through_all_to_some_with_negated_inner() {
308 let mut p = pool();
310 let r = Role::named(RoleId::new(0));
311 let a = p.atomic(ClassId::new(0));
312 let all_a = p.all(r, a);
313 let not_all = p.not(all_a);
314 let result = to_nnf(not_all, &mut p);
315 let na = p.not(a);
316 let expected = p.some(r, na);
317 assert_eq!(result, expected);
318 }
319
320 #[test]
321 fn nnf_of_not_min_positive_n_becomes_max_n_minus_one() {
322 let mut p = pool();
324 let r = Role::named(RoleId::new(0));
325 let a = p.atomic(ClassId::new(0));
326 let min3 = p.min(3, r, a);
327 let not_min3 = p.not(min3);
328 let result = to_nnf(not_min3, &mut p);
329 let expected = p.max(2, r, a);
330 assert_eq!(result, expected);
331 }
332
333 #[test]
334 fn nnf_of_not_min_zero_is_bot() {
335 let mut p = pool();
337 let r = Role::named(RoleId::new(0));
338 let a = p.atomic(ClassId::new(0));
339 let min0 = p.min(0, r, a);
340 let not_min0 = p.not(min0);
341 let result = to_nnf(not_min0, &mut p);
342 assert_eq!(result, p.bot());
343 }
344
345 #[test]
346 fn nnf_of_not_max_becomes_min_n_plus_one() {
347 let mut p = pool();
349 let r = Role::named(RoleId::new(0));
350 let a = p.atomic(ClassId::new(0));
351 let max2 = p.max(2, r, a);
352 let not_max2 = p.not(max2);
353 let result = to_nnf(not_max2, &mut p);
354 let expected = p.min(3, r, a);
355 assert_eq!(result, expected);
356 }
357
358 #[test]
359 fn nnf_of_not_nominal_keeps_one_not() {
360 let mut p = pool();
362 let n = p.nominal(IndividualId::new(0));
363 let not_n = p.not(n);
364 assert_eq!(to_nnf(not_n, &mut p), not_n);
365 }
366
367 #[test]
368 fn nnf_of_not_self_restriction_keeps_one_not() {
369 let mut p = pool();
370 let r = Role::named(RoleId::new(0));
371 let s = p.self_restriction(r);
372 let not_s = p.not(s);
373 assert_eq!(to_nnf(not_s, &mut p), not_s);
374 }
375
376 #[test]
377 fn nested_negation_through_nested_structure() {
378 let mut p = pool();
380 let r = Role::named(RoleId::new(0));
381 let a = p.atomic(ClassId::new(0));
382 let b = p.atomic(ClassId::new(1));
383 let some_b = p.some(r, b);
384 let conj = p.and([a, some_b]);
385 let neg = p.not(conj);
386 let result = to_nnf(neg, &mut p);
387 let na = p.not(a);
388 let nb = p.not(b);
389 let all_nb = p.all(r, nb);
390 let expected = p.or([na, all_nb]);
391 assert_eq!(result, expected);
392 }
393
394 #[test]
395 fn is_nnf_recognizes_nnf_forms() {
396 let mut p = pool();
397 let r = Role::named(RoleId::new(0));
398 let a = p.atomic(ClassId::new(0));
399 let na = p.not(a);
400 let conj = p.and([a, na]);
401 assert!(is_nnf(conj, &p));
402 let all_na = p.all(r, na);
403 assert!(is_nnf(all_na, &p));
404 }
405
406 #[test]
407 fn is_nnf_rejects_non_atomic_negation() {
408 let mut p = pool();
410 let a = p.atomic(ClassId::new(0));
411 let b = p.atomic(ClassId::new(1));
412 let conj = p.and([a, b]);
413 let neg = p.not(conj);
414 assert!(!is_nnf(neg, &p));
415 }
416
417 use crate::ontology::{Axiom, InternalOntology};
420
421 fn fresh_ontology() -> InternalOntology {
422 let mut o = InternalOntology::new();
423 o.vocabulary.intern_class("A");
424 o.vocabulary.intern_class("B");
425 o.vocabulary.intern_class("C");
426 o
427 }
428
429 #[test]
430 fn nnf_axioms_rewrites_sub_class_of() {
431 let mut o = fresh_ontology();
433 let a = o.concepts.atomic(ClassId::new(0));
434 let b = o.concepts.atomic(ClassId::new(1));
435 let c = o.concepts.atomic(ClassId::new(2));
436 let and_bc = o.concepts.and([b, c]);
437 let not_and = o.concepts.not(and_bc);
438 o.axioms.push(Axiom::SubClassOf {
439 sub: a,
440 sup: not_and,
441 });
442 let normalized = nnf_axioms(&mut o);
443 let Axiom::SubClassOf { sub, sup } = &normalized[0] else {
444 panic!()
445 };
446 assert_eq!(*sub, a);
447 let nb = o.concepts.not(b);
448 let nc = o.concepts.not(c);
449 let expected_sup = o.concepts.or([nb, nc]);
450 assert_eq!(*sup, expected_sup);
451 assert!(is_nnf(*sup, &o.concepts));
452 }
453
454 #[test]
455 fn nnf_axioms_leaves_original_axioms_unchanged() {
456 let mut o = fresh_ontology();
458 let a = o.concepts.atomic(ClassId::new(0));
459 let b = o.concepts.atomic(ClassId::new(1));
460 let and_ab = o.concepts.and([a, b]);
461 let not_and = o.concepts.not(and_ab);
462 o.axioms.push(Axiom::SubClassOf {
463 sub: a,
464 sup: not_and,
465 });
466 let original_count = o.axioms.len();
467 let _ = nnf_axioms(&mut o);
468 assert_eq!(o.axioms.len(), original_count);
469 let Axiom::SubClassOf { sup, .. } = &o.axioms[0] else {
471 panic!()
472 };
473 assert_eq!(*sup, not_and);
474 }
475
476 #[test]
477 fn nnf_axioms_handles_multi_concept_axioms() {
478 let mut o = fresh_ontology();
480 let a = o.concepts.atomic(ClassId::new(0));
481 let b = o.concepts.atomic(ClassId::new(1));
482 let not_a = o.concepts.not(a);
483 let not_not_a = o.concepts.not(not_a);
484 o.axioms.push(Axiom::EquivalentClasses(vec![not_not_a, b]));
485 let normalized = nnf_axioms(&mut o);
486 let Axiom::EquivalentClasses(ids) = &normalized[0] else {
487 panic!()
488 };
489 assert_eq!(ids[0], a);
491 assert_eq!(ids[1], b);
492 }
493
494 #[test]
495 fn nnf_axioms_passes_through_role_axioms_unchanged() {
496 let mut o = fresh_ontology();
497 let r = Role::named(RoleId::new(0));
498 o.axioms.push(Axiom::TransitiveRole(r));
499 let normalized = nnf_axioms(&mut o);
500 assert!(matches!(normalized[0], Axiom::TransitiveRole(_)));
501 }
502
503 #[test]
504 fn nnf_axioms_normalizes_class_assertion_concept() {
505 let mut o = fresh_ontology();
506 let i = o.vocabulary.intern_individual("a");
507 let a = o.concepts.atomic(ClassId::new(0));
508 let not_a = o.concepts.not(a);
509 let not_not_a = o.concepts.not(not_a);
510 o.axioms.push(Axiom::ClassAssertion {
511 class: not_not_a,
512 individual: i,
513 });
514 let normalized = nnf_axioms(&mut o);
515 let Axiom::ClassAssertion { class, .. } = normalized[0] else {
516 panic!()
517 };
518 assert_eq!(class, a);
519 }
520}