Skip to content

Commit 11585bf

Browse files
committed
Merge remote-tracking branch 'origin/develop' into ontology-save-formats
2 parents e32f979 + ea7054c commit 11585bf

5 files changed

Lines changed: 730 additions & 32 deletions

File tree

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,41 @@ justifications = reasoner.create_axiom_justifications(axiom)
388388
</details>
389389

390390

391+
### Get Contrastive Explanations
392+
393+
<details><summary> Click me!</summary>
394+
395+
```python
396+
from owlapy import manchester_to_owl_expression
397+
from owlapy.iri import IRI
398+
from owlapy.owl_individual import OWLNamedIndividual
399+
from owlapy.owl_ontology import SyncOntology
400+
from owlapy.owl_reasoner import SyncReasoner
401+
402+
# --- Load ontology and reasoner ---
403+
ontology = SyncOntology("../KGs/Family/family.owl")
404+
reasoner = SyncReasoner(ontology, reasoner="HermiT")
405+
406+
# --- Define class expression ---
407+
class_expr = manchester_to_owl_expression(
408+
"Sister and (hasSibling some (married some (hasChild some Grandchild)))",
409+
"http://www.benchmark.org/family#"
410+
)
411+
412+
# --- Define individuals ---
413+
fact = OWLNamedIndividual(IRI.create("http://www.benchmark.org/family#F9F143"))
414+
foil = OWLNamedIndividual(IRI.create("http://www.benchmark.org/family#F9M161"))
415+
416+
# --- Get contrastive explanation ---
417+
result = reasoner.get_contrastive_explanation(class_expr, fact, foil)
418+
419+
# --- Print results ---
420+
for k in ["common", "different", "conflict"]:
421+
print(f"{k.capitalize()} axioms: {result[k]}")
422+
```
423+
</details>
424+
425+
391426
### Class expression simplification
392427

393428
<details><summary> Click me!</summary>

owlapy/converter.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ def convert(self, root_variable: str,
132132
self.for_all_de_morgan = for_all_de_morgan
133133
self.named_individuals = named_individuals
134134
# # if named_individuals is True, we return only entities that are instances of owl:NamedIndividual
135-
# if named_individuals:
136-
# self.append_triple(root_variable, 'a', f"<{OWLRDFVocabulary.OWL_NAMED_INDIVIDUAL.as_str()}>")
135+
if named_individuals:
136+
self.append_triple(root_variable, 'a', f"<{OWLRDFVocabulary.OWL_NAMED_INDIVIDUAL.as_str()}>")
137137
with self.stack_variable(root_variable):
138138
with self.stack_parent(ce):
139139
self.process(ce)
@@ -270,11 +270,7 @@ def _(self, ce: OWLObjectComplementOf):
270270
# the exclusion of "?x ?p ?o" results in the group graph pattern to just return true or false (not bindings)
271271
# as a result, we need to comment out the if-clause of the following line
272272
# if not self.in_intersection and self.modal_depth == 1:
273-
# if namedIndividual is set to True, do not use variables --> restrict the subject to instances of NamedIndividual
274-
if self.named_individuals:
275-
self.append_triple(subject, "a", f"<{OWLRDFVocabulary.OWL_NAMED_INDIVIDUAL.as_str()}>")
276-
else:
277-
self.append_triple(subject, self.mapping.new_individual_variable(), self.mapping.new_individual_variable())
273+
self.append_triple(subject, self.mapping.new_individual_variable(), self.mapping.new_individual_variable())
278274

279275
self.append("FILTER NOT EXISTS { ")
280276
# process the concept after the ¬
@@ -557,7 +553,9 @@ def _(self, ce: OWLDataHasValue):
557553
@process.register
558554
def _(self, node: OWLDatatype):
559555
if node != TopOWLDatatype:
560-
self.append(f" FILTER ( DATATYPE ( {self.current_variable} = <{node.to_string_id()}> ) ) ")
556+
self.append(f" FILTER ( DATATYPE ( {self.current_variable} ) = <{node.to_string_id()}> ) ")
557+
else:
558+
self.append(f" FILTER ( isLiteral ( {self.current_variable} ) ")
561559

562560
@process.register
563561
def _(self, node: OWLDataOneOf):
@@ -627,6 +625,8 @@ def as_query(self,
627625
q.append(f"<{x.to_string_id()}>")
628626
q.append("} . ")
629627
qs.extend(q)
628+
if named_individuals:
629+
qs.append(f"{root_variable} a <{OWLRDFVocabulary.OWL_NAMED_INDIVIDUAL.as_str()}> . ")
630630
qs.extend(tp)
631631
qs.append(" }")
632632

@@ -643,10 +643,12 @@ def as_confusion_matrix_query(self,
643643
for_all_de_morgan: bool = True,
644644
named_individuals: bool = False) -> str:
645645
# get the graph pattern corresponding to the provided class expression (ce)
646-
graph_pattern_str = "".join(self.convert(root_variable,
647-
ce,
648-
for_all_de_morgan=for_all_de_morgan,
649-
named_individuals=named_individuals))
646+
tp = self.convert(root_variable, ce, for_all_de_morgan=for_all_de_morgan, named_individuals=named_individuals)
647+
if named_individuals:
648+
named_individual_triple = f"{root_variable} a <{OWLRDFVocabulary.OWL_NAMED_INDIVIDUAL.as_str()}> . "
649+
graph_pattern_str = named_individual_triple + "".join(tp)
650+
else:
651+
graph_pattern_str = "".join(tp)
650652
# preparation for the final query
651653

652654
# required to compute false negatives

owlapy/owl_reasoner.py

Lines changed: 178 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
OWLObjectIntersectionOf, OWLObjectComplementOf, OWLObjectAllValuesFrom, OWLObjectOneOf, OWLObjectHasValue, \
1919
OWLObjectMinCardinality, OWLObjectMaxCardinality, OWLObjectExactCardinality, OWLObjectCardinalityRestriction, \
2020
OWLDataSomeValuesFrom, OWLDataOneOf, OWLDatatypeRestriction, OWLFacetRestriction, OWLDataHasValue, \
21-
OWLDataAllValuesFrom, OWLNothing, OWLThing
21+
OWLDataAllValuesFrom, OWLNothing, OWLThing, OWLDataMinCardinality, OWLDataMaxCardinality, OWLDataExactCardinality
2222
from owlapy.class_expression import OWLClass
2323
from owlapy.iri import IRI
2424
from owlapy.owl_axiom import OWLAxiom, OWLSubClassOfAxiom
@@ -989,6 +989,183 @@ def _lazy_cache_class(self, c: OWLClass) -> None:
989989
temp = self.get_instances_from_owl_class(c)
990990
self._cls_to_ind[c] = frozenset(temp)
991991

992+
@_find_instances.register
993+
def _(self, ce: OWLDataMinCardinality):
994+
return self._get_instances_data_card_restriction(ce)
995+
996+
@_find_instances.register
997+
def _(self, ce: OWLDataMaxCardinality):
998+
all_ = frozenset(self._ontology.individuals_in_signature())
999+
min_ind = self._get_instances_data_card_restriction(
1000+
OWLDataMinCardinality(cardinality=ce.get_cardinality() + 1,
1001+
property=ce.get_property(),
1002+
filler=ce.get_filler()))
1003+
return all_ ^ min_ind
1004+
1005+
@_find_instances.register
1006+
def _(self, ce: OWLDataExactCardinality):
1007+
return self._get_instances_data_card_restriction(ce)
1008+
1009+
def _get_instances_data_card_restriction(self, ce) -> FrozenSet[OWLNamedIndividual]:
1010+
"""Get instances for OWLDataMinCardinality or OWLDataExactCardinality restrictions."""
1011+
pe = ce.get_property()
1012+
filler = ce.get_filler()
1013+
assert isinstance(pe, OWLDataProperty)
1014+
1015+
if isinstance(ce, OWLDataMinCardinality):
1016+
min_count = ce.get_cardinality()
1017+
max_count = None
1018+
elif isinstance(ce, OWLDataExactCardinality):
1019+
min_count = max_count = ce.get_cardinality()
1020+
else:
1021+
raise NotImplementedError
1022+
1023+
assert min_count >= 0
1024+
assert max_count is None or max_count >= 0
1025+
1026+
if self._ontology.is_modified and (self.class_cache or self._property_cache):
1027+
self.reset_and_disable_cache()
1028+
property_cache = self._property_cache
1029+
1030+
if property_cache:
1031+
self._lazy_cache_data_prop(pe)
1032+
dps = self._data_prop[pe]
1033+
else:
1034+
subs = self._some_values_subject_index(pe)
1035+
1036+
ind = set()
1037+
1038+
if isinstance(filler, OWLDatatype):
1039+
if property_cache:
1040+
for s, literals in dps.items():
1041+
count = sum(1 for lit in literals if lit.get_datatype() == filler)
1042+
if count >= min_count and (max_count is None or count <= max_count):
1043+
ind.add(s)
1044+
else:
1045+
for s in subs:
1046+
count = sum(1 for lit in self.data_property_values(s, pe) if lit.get_datatype() == filler)
1047+
if count >= min_count and (max_count is None or count <= max_count):
1048+
ind.add(s)
1049+
elif isinstance(filler, OWLDataOneOf):
1050+
values = set(filler.values())
1051+
if property_cache:
1052+
for s, literals in dps.items():
1053+
count = len(literals & values)
1054+
if count >= min_count and (max_count is None or count <= max_count):
1055+
ind.add(s)
1056+
else:
1057+
for s in subs:
1058+
count = sum(1 for lit in self.data_property_values(s, pe) if lit in values)
1059+
if count >= min_count and (max_count is None or count <= max_count):
1060+
ind.add(s)
1061+
elif isinstance(filler, OWLDatatypeRestriction):
1062+
def res_to_callable(res: OWLFacetRestriction):
1063+
op = res.get_facet().operator
1064+
v = res.get_facet_value()
1065+
1066+
def inner(lv: OWLLiteral):
1067+
return op(lv, v)
1068+
1069+
return inner
1070+
1071+
apply = FunctionType.__call__
1072+
facet_restrictions = tuple(map(res_to_callable, filler.get_facet_restrictions()))
1073+
1074+
def include(lv: OWLLiteral):
1075+
return lv.get_datatype() == filler.get_datatype() and \
1076+
all(map(apply, facet_restrictions, repeat(lv)))
1077+
1078+
if property_cache:
1079+
for s, literals in dps.items():
1080+
count = sum(1 for lit in literals if include(lit))
1081+
if count >= min_count and (max_count is None or count <= max_count):
1082+
ind.add(s)
1083+
else:
1084+
for s in subs:
1085+
count = sum(1 for lit in self.data_property_values(s, pe) if include(lit))
1086+
if count >= min_count and (max_count is None or count <= max_count):
1087+
ind.add(s)
1088+
elif isinstance(filler, OWLDataComplementOf):
1089+
# Count values NOT matching the inner data range
1090+
inner_filler = filler.get_data_range()
1091+
# some_inner = OWLDataSomeValuesFrom(property=pe, filler=inner_filler)
1092+
# We need to count per-individual, so we iterate
1093+
if property_cache:
1094+
for s, literals in dps.items():
1095+
# Count literals that match the complement (i.e., do NOT match the inner filler)
1096+
match_inner = self._count_data_filler_matches(literals, inner_filler)
1097+
count = len(literals) - match_inner
1098+
if count >= min_count and (max_count is None or count <= max_count):
1099+
ind.add(s)
1100+
else:
1101+
for s in subs:
1102+
all_lits = set(self.data_property_values(s, pe))
1103+
match_inner = self._count_data_filler_matches(all_lits, inner_filler)
1104+
count = len(all_lits) - match_inner
1105+
if count >= min_count and (max_count is None or count <= max_count):
1106+
ind.add(s)
1107+
elif isinstance(filler, OWLDataUnionOf):
1108+
if property_cache:
1109+
for s, literals in dps.items():
1110+
count = sum(1 for lit in literals
1111+
if any(self._literal_matches_data_range(lit, op)
1112+
for op in filler.operands()))
1113+
if count >= min_count and (max_count is None or count <= max_count):
1114+
ind.add(s)
1115+
else:
1116+
for s in subs:
1117+
count = sum(1 for lit in self.data_property_values(s, pe)
1118+
if any(self._literal_matches_data_range(lit, op)
1119+
for op in filler.operands()))
1120+
if count >= min_count and (max_count is None or count <= max_count):
1121+
ind.add(s)
1122+
elif isinstance(filler, OWLDataIntersectionOf):
1123+
if property_cache:
1124+
for s, literals in dps.items():
1125+
count = sum(1 for lit in literals
1126+
if all(self._literal_matches_data_range(lit, op)
1127+
for op in filler.operands()))
1128+
if count >= min_count and (max_count is None or count <= max_count):
1129+
ind.add(s)
1130+
else:
1131+
for s in subs:
1132+
count = sum(1 for lit in self.data_property_values(s, pe)
1133+
if all(self._literal_matches_data_range(lit, op)
1134+
for op in filler.operands()))
1135+
if count >= min_count and (max_count is None or count <= max_count):
1136+
ind.add(s)
1137+
else:
1138+
raise ValueError
1139+
1140+
return frozenset(ind)
1141+
1142+
def _literal_matches_data_range(self, lit: OWLLiteral, dr) -> bool:
1143+
"""Check if a single literal matches a data range."""
1144+
if isinstance(dr, OWLDatatype):
1145+
return lit.get_datatype() == dr
1146+
elif isinstance(dr, OWLDataOneOf):
1147+
return lit in set(dr.values())
1148+
elif isinstance(dr, OWLDatatypeRestriction):
1149+
if lit.get_datatype() != dr.get_datatype():
1150+
return False
1151+
for res in dr.get_facet_restrictions():
1152+
op = res.get_facet().operator
1153+
if not op(lit, res.get_facet_value()):
1154+
return False
1155+
return True
1156+
elif isinstance(dr, OWLDataComplementOf):
1157+
return not self._literal_matches_data_range(lit, dr.get_data_range())
1158+
elif isinstance(dr, OWLDataUnionOf):
1159+
return any(self._literal_matches_data_range(lit, op) for op in dr.operands())
1160+
elif isinstance(dr, OWLDataIntersectionOf):
1161+
return all(self._literal_matches_data_range(lit, op) for op in dr.operands())
1162+
else:
1163+
raise ValueError(f"Unsupported data range type: {type(dr)}")
1164+
1165+
def _count_data_filler_matches(self, literals: Set[OWLLiteral], dr) -> int:
1166+
"""Count how many literals in the set match the given data range."""
1167+
return sum(1 for lit in literals if self._literal_matches_data_range(lit, dr))
1168+
9921169
def get_instances_from_owl_class(self, c: OWLClass):
9931170
if c.is_owl_thing():
9941171
yield from self._ontology.individuals_in_signature()

owlapy/utils.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -952,28 +952,32 @@ def _merge_card_r_with_same_body(self, restriction, nary_ce = None):
952952
filler=self._simplify(restriction.get_filler()))
953953

954954
def _process_cardinality_restriction(self, restriction, nary_ce = None):
955-
# Check for card restrictions that have the share the same cardinality and property.
956-
# They can be simplified into a single card restriction with a merged filler.
957-
# E.g.:
958-
# (> 1 r.A) ⊔ (> 1 r.B) ≡ > 1 r.(A ⊔ B)
959-
# (< 2 r.xsd:boolean) ⊔ (< 2 r.xsd:integer) ≡ < 2 r.(xsd:boolean ⊔ xsd:integer)
955+
# Check for card restrictions that share the same cardinality and property.
956+
# They can be simplified into a single card restriction with a merged filler,
957+
# but only for scenarios where the equivalence actually holds:
958+
#
959+
# For OWLObjectMinCardinality / OWLDataMinCardinality with cardinality = 1 in a union:
960+
# (≥ 1 r.A) ⊔ (≥ 1 r.B) ≡ ≥ 1 r.(A ⊔ B) ← valid (equivalent to ∃r)
960961
if isinstance(nary_ce, OWLObjectUnionOf):
961-
operands = set(nary_ce.operands())
962-
same_root = []
963-
for op in operands:
964-
if (isinstance(op, type(restriction))
965-
and op.get_cardinality() == restriction.get_cardinality()
966-
and op.get_property() == restriction.get_property()):
967-
same_root.append(op)
968-
if not len(same_root) == 1:
969-
fillers = _sort_by_ordered_owl_object([p.get_filler() for p in same_root])
970-
if isinstance(list(fillers)[0],OWLDataRange):
962+
# Only apply filler merging for min-cardinality restrictions with cardinality == 1
963+
if (isinstance(restriction, (OWLObjectMinCardinality, OWLDataMinCardinality))
964+
and restriction.get_cardinality() == 1):
965+
operands = set(nary_ce.operands())
966+
same_root = []
967+
for op in operands:
968+
if (isinstance(op, type(restriction))
969+
and op.get_cardinality() == restriction.get_cardinality()
970+
and op.get_property() == restriction.get_property()):
971+
same_root.append(op)
972+
if not len(same_root) == 1:
973+
fillers = _sort_by_ordered_owl_object([p.get_filler() for p in same_root])
974+
if isinstance(list(fillers)[0], OWLDataRange):
975+
return type(restriction)(cardinality=restriction.get_cardinality(),
976+
property=restriction.get_property(),
977+
filler=self._simplify(OWLDataUnionOf(fillers)))
971978
return type(restriction)(cardinality=restriction.get_cardinality(),
972979
property=restriction.get_property(),
973-
filler=self._simplify(OWLDataUnionOf(fillers)))
974-
return type(restriction)(cardinality=restriction.get_cardinality(),
975-
property=restriction.get_property(),
976-
filler=self._simplify(OWLObjectUnionOf(fillers)))
980+
filler=self._simplify(OWLObjectUnionOf(fillers)))
977981
return type(restriction)(cardinality=restriction.get_cardinality(),
978982
property=restriction.get_property(),
979983
filler=self._simplify(restriction.get_filler(), None))

0 commit comments

Comments
 (0)