From 27fa81fb9e1fec83f6f4a8f98a8e7a8c4c20d5fa Mon Sep 17 00:00:00 2001 From: Jim Balhoff Date: Thu, 26 Sep 2024 12:19:13 -0400 Subject: [PATCH] Add support for ObjectPropertyRange in EL reasoner. --- .../scala/org/geneontology/whelk/Model.scala | 23 ++- .../org/geneontology/whelk/Reasoner.scala | 34 +++- .../scala/org/geneontology/whelk/Bridge.scala | 16 +- .../geneontology/whelk/part-of-arm-ranges.ofn | 137 +++++++++++++++++ .../whelk/part-of-arm-ranges2.ofn | 145 ++++++++++++++++++ .../geneontology/whelk/TestInferences.scala | 2 + 6 files changed, 347 insertions(+), 10 deletions(-) create mode 100644 modules/owlapi/src/test/resources/org/geneontology/whelk/part-of-arm-ranges.ofn create mode 100644 modules/owlapi/src/test/resources/org/geneontology/whelk/part-of-arm-ranges2.ofn diff --git a/modules/core/src/main/scala/org/geneontology/whelk/Model.scala b/modules/core/src/main/scala/org/geneontology/whelk/Model.scala index 8074d8e..8c4c5c1 100644 --- a/modules/core/src/main/scala/org/geneontology/whelk/Model.scala +++ b/modules/core/src/main/scala/org/geneontology/whelk/Model.scala @@ -205,8 +205,6 @@ final case class Individual(id: String) extends Entity with IndividualArgument { } -sealed trait Axiom extends HasSignature - final case class Nominal(individual: Individual) extends Concept { def conceptSignature: Set[Concept] = Set(this) @@ -219,6 +217,21 @@ final case class Nominal(individual: Individual) extends Concept { } + +final case class RoleTarget(role: Role, concept: Concept) extends Concept { + + def conceptSignature: Set[Concept] = concept.conceptSignature + this + + def signature: Set[Entity] = concept.signature + role + + def isAnonymous: Boolean = true + + override val hashCode: Int = scala.util.hashing.MurmurHash3.productHash(this) + +} + +sealed trait Axiom extends HasSignature + final case class ConceptInclusion(subclass: Concept, superclass: Concept) extends Axiom with QueueExpression { def signature: Set[Entity] = subclass.signature ++ superclass.signature @@ -237,6 +250,12 @@ final case class RoleComposition(first: Role, second: Role, superproperty: Role) } +final case class RoleHasRange(role: Role, range: Concept) extends Axiom { + + def signature: Set[Entity] = range.signature + role + +} + final case class ConceptAssertion(concept: Concept, individual: Individual) extends Axiom { def signature: Set[Entity] = concept.signature + individual diff --git a/modules/core/src/main/scala/org/geneontology/whelk/Reasoner.scala b/modules/core/src/main/scala/org/geneontology/whelk/Reasoner.scala index 38cf3fd..898189c 100644 --- a/modules/core/src/main/scala/org/geneontology/whelk/Reasoner.scala +++ b/modules/core/src/main/scala/org/geneontology/whelk/Reasoner.scala @@ -11,6 +11,7 @@ final case class ReasonerState( hier: Map[Role, Set[Role]] = Map.empty, // initial hierList: Map[Role, List[Role]] = Map.empty, // initial hierComps: Map[Role, Map[Role, List[Role]]] = Map.empty, // initial + roleRanges: Map[Role, Concept] = Map.empty, // initial assertions: List[ConceptInclusion] = Nil, inits: Set[Concept] = Set.empty, // closure assertedConceptInclusionsBySubclass: Map[Concept, List[ConceptInclusion]] = Map.empty, @@ -126,6 +127,13 @@ object Reasoner { val hier: Map[Role, Set[Role]] = saturateRoles(allRoleInclusions) |+| allRoles.map(r => r -> Set(r)).toMap val hierList = hier.map { case (k, v) => k -> v.toList } val hierComps = indexRoleCompositions(hier, axioms.collect { case rc: RoleComposition => rc }) + val allRoleRangeAxioms = axioms.collect { case rr: RoleHasRange => rr } + val assertedRoleRanges = allRoleRangeAxioms.groupBy(_.role).view.mapValues(_.map(_.range)).toMap + val roleRanges = for { + (subprop, supers) <- hier + ranges = supers.flatMap(sup => assertedRoleRanges.get(sup).toSet.flatten) + if ranges.nonEmpty + } yield subprop -> (if (ranges.size == 1) ranges.head else ranges.reduce(Conjunction)) val rules = axioms.collect { case r: Rule => r } val anonymousRulePredicates = rules.flatMap(_.body.collect { case ConceptAtom(concept, _) if concept.isAnonymous => ConceptInclusion(concept, Top) @@ -133,7 +141,7 @@ object Reasoner { val concIncs = axioms.collect { case ci: ConceptInclusion => ci } ++ anonymousRulePredicates val ruleEngine = RuleEngine(rules) val wm = ruleEngine.emptyMemory - assert(concIncs, ReasonerState.empty.copy(hier = hier, hierList = hierList, hierComps = hierComps, ruleEngine = ruleEngine, wm = wm, queueDelegates = delegates, disableBottom = disableBottom)) + assert(concIncs, ReasonerState.empty.copy(hier = hier, hierList = hierList, hierComps = hierComps, roleRanges = roleRanges, ruleEngine = ruleEngine, wm = wm, queueDelegates = delegates, disableBottom = disableBottom)) } def assert(axioms: Set[ConceptInclusion], reasoner: ReasonerState): ReasonerState = { @@ -267,7 +275,16 @@ object Reasoner { } private[this] def R0(concept: Concept, reasoner: ReasonerState, todo: mutable.Stack[QueueExpression]): ReasonerState = { - todo.push(ConceptInclusion(concept, concept)) + concept match { + case rt @ RoleTarget(role, target) => + for { + range <- reasoner.roleRanges.get(role) + } { + todo.push(ConceptInclusion(rt, range)) + todo.push(ConceptInclusion(rt, target)) + } + case _ => todo.push(ConceptInclusion(concept, concept)) + } reasoner } @@ -424,7 +441,15 @@ object Reasoner { private[this] def `R-∃`(ci: ConceptInclusion, reasoner: ReasonerState, todo: mutable.Stack[QueueExpression]): ReasonerState = ci match { case ConceptInclusion(c, ExistentialRestriction(role, filler)) => - todo.push(Link(c, role, filler)) + val target = (c, filler) match { + case (Nominal(_), Nominal(_)) => + // ranges will be handled by rete rules + filler + case _ => + if (reasoner.roleRanges.contains(role)) RoleTarget(role, filler) + else filler + } + todo.push(Link(c, role, target)) reasoner case _ => reasoner } @@ -637,6 +662,9 @@ object Reasoner { private[this] def `R-⟲`(ci: ConceptInclusion, reasoner: ReasonerState, todo: mutable.Stack[QueueExpression]): ReasonerState = ci match { case ConceptInclusion(sub, SelfRestriction(role)) => + reasoner.roleRanges.get(role).foreach { range => + todo.push(ConceptInclusion(sub, range)) + } todo.push(Link(sub, role, sub)) reasoner case _ => reasoner diff --git a/modules/owlapi/src/main/scala/org/geneontology/whelk/Bridge.scala b/modules/owlapi/src/main/scala/org/geneontology/whelk/Bridge.scala index ac2236d..57d8090 100644 --- a/modules/owlapi/src/main/scala/org/geneontology/whelk/Bridge.scala +++ b/modules/owlapi/src/main/scala/org/geneontology/whelk/Bridge.scala @@ -133,11 +133,17 @@ object Bridge { Set( Rule(body = List(RoleAtom(role, WVariable("x"), WVariable("y")), RoleAtom(role, WVariable("y"), WVariable("x"))), head = List(ConceptAtom(BuiltIn.Bottom, WVariable("x")), ConceptAtom(BuiltIn.Bottom, WVariable("y")))) ) - case ObjectPropertyDomain(_, ObjectProperty(property), ce) => convertExpression(ce).map(concept => - ConceptInclusion(ExistentialRestriction(Role(property.toString), Top), concept)).toSet - case ObjectPropertyRange(_, ObjectProperty(property), ce) => convertExpression(ce).map(concept => - //TODO only supporting in rules for now - Rule(body = List(RoleAtom(Role(property.toString), WVariable("x1"), WVariable("x2"))), head = List(ConceptAtom(concept, WVariable("x2"))))).toSet + case ObjectPropertyDomain(_, ObjectProperty(property), ce) => + convertExpression(ce).map(concept => + ConceptInclusion(ExistentialRestriction(Role(property.toString), Top), concept)).toSet + case ObjectPropertyRange(_, ObjectProperty(property), ce) => + convertExpression(ce).to(Set).flatMap { concept => + val role = Role(property.toString) + Set( + RoleHasRange(role, concept), + Rule(body = List(RoleAtom(role, WVariable("x1"), WVariable("x2"))), head = List(ConceptAtom(concept, WVariable("x2")))) + ) + } case InverseObjectProperties(_, ObjectProperty(p), ObjectProperty(q)) => val (roleP, roleQ) = (Role(p.toString), Role(q.toString)) val (x1, x2) = (WVariable("x1"), WVariable("x2")) diff --git a/modules/owlapi/src/test/resources/org/geneontology/whelk/part-of-arm-ranges.ofn b/modules/owlapi/src/test/resources/org/geneontology/whelk/part-of-arm-ranges.ofn new file mode 100644 index 0000000..d75c795 --- /dev/null +++ b/modules/owlapi/src/test/resources/org/geneontology/whelk/part-of-arm-ranges.ofn @@ -0,0 +1,137 @@ +Prefix(:=) +Prefix(owl:=) +Prefix(rdf:=) +Prefix(xml:=) +Prefix(xsd:=) +Prefix(rdfs:=) + + +Ontology( + +Declaration(Class(:A)) +Declaration(Class(:B)) +Declaration(Class(:C)) +Declaration(Class(:D)) +Declaration(Class(:arm)) +Declaration(Class(:finger)) +Declaration(Class(:hand)) +Declaration(Class(:part_of_arm)) +Declaration(Class(:part_of_some_A)) +Declaration(Class(:q_some_C)) +Declaration(Class(:r_some_hand)) +Declaration(Class(:reflexive_part_of_some_arm)) +Declaration(Class(:s_some_Self)) +Declaration(Class(:special_arm)) +Declaration(Class(:special_hand)) +Declaration(Class(:t_some_Self)) +Declaration(ObjectProperty(:p)) +Declaration(ObjectProperty(:part_of)) +Declaration(ObjectProperty(:q)) +Declaration(ObjectProperty(:r)) +Declaration(ObjectProperty(:reflexive_part_of)) +Declaration(ObjectProperty(:s)) +Declaration(ObjectProperty(:t)) +Declaration(NamedIndividual(:a)) +############################ +# Object Properties +############################ + +# Object Property: :p (:p) + +ObjectPropertyRange(:p :C) + +# Object Property: :part_of (:part_of) + +SubObjectPropertyOf(:part_of :reflexive_part_of) +TransitiveObjectProperty(:part_of) +ObjectPropertyRange(:part_of :A) + +# Object Property: :q (:q) + +SubObjectPropertyOf(:q :p) + +# Object Property: :r (:r) + +ObjectPropertyRange(:r :B) + +# Object Property: :reflexive_part_of (:reflexive_part_of) + +ReflexiveObjectProperty(:reflexive_part_of) + +# Object Property: :t (:t) + +SubObjectPropertyOf(:t :s) + + +############################ +# Classes +############################ + +# Class: :A (:A) + +SubClassOf(:A ObjectSomeValuesFrom(:q :B)) + +# Class: :D (:D) + +SubClassOf(:D ObjectSomeValuesFrom(:t :D)) + +# Class: :arm (:arm) + +SubClassOf(:arm ObjectSomeValuesFrom(:r :hand)) + +# Class: :finger (:finger) + +SubClassOf(:finger ObjectSomeValuesFrom(:part_of :special_hand)) + +# Class: :hand (:hand) + +SubClassOf(:hand ObjectSomeValuesFrom(:part_of :special_arm)) +SubClassOf(:hand ObjectHasSelf(:r)) + +# Class: :part_of_arm (:part_of_arm) + +EquivalentClasses(:part_of_arm ObjectSomeValuesFrom(:part_of :arm)) + +# Class: :part_of_some_A (:part_of_some_A) + +EquivalentClasses(:part_of_some_A ObjectSomeValuesFrom(:part_of :A)) + +# Class: :q_some_C (:q_some_C) + +EquivalentClasses(:q_some_C ObjectSomeValuesFrom(:q :C)) + +# Class: :r_some_hand (:r_some_hand) + +EquivalentClasses(:r_some_hand ObjectSomeValuesFrom(:r :hand)) + +# Class: :reflexive_part_of_some_arm (:reflexive_part_of_some_arm) + +EquivalentClasses(:reflexive_part_of_some_arm ObjectSomeValuesFrom(:reflexive_part_of :arm)) + +# Class: :s_some_Self (:s_some_Self) + +EquivalentClasses(:s_some_Self ObjectHasSelf(:s)) + +# Class: :special_arm (:special_arm) + +SubClassOf(:special_arm :arm) + +# Class: :special_hand (:special_hand) + +SubClassOf(:special_hand :hand) + +# Class: :t_some_Self (:t_some_Self) + +EquivalentClasses(:t_some_Self ObjectHasSelf(:t)) + + +############################ +# Named Individuals +############################ + +# Individual: :a (:a) + +ObjectPropertyAssertion(:t :a :a) + + +) \ No newline at end of file diff --git a/modules/owlapi/src/test/resources/org/geneontology/whelk/part-of-arm-ranges2.ofn b/modules/owlapi/src/test/resources/org/geneontology/whelk/part-of-arm-ranges2.ofn new file mode 100644 index 0000000..214a976 --- /dev/null +++ b/modules/owlapi/src/test/resources/org/geneontology/whelk/part-of-arm-ranges2.ofn @@ -0,0 +1,145 @@ +Prefix(:=) +Prefix(owl:=) +Prefix(rdf:=) +Prefix(xml:=) +Prefix(xsd:=) +Prefix(rdfs:=) + + +Ontology( + +Declaration(Class()) +Declaration(Class()) +Declaration(Class()) +Declaration(Class(:A)) +Declaration(Class(:B)) +Declaration(Class(:C)) +Declaration(Class(:D)) +Declaration(Class(:arm)) +Declaration(Class(:finger)) +Declaration(Class(:hand)) +Declaration(Class(:part_of_arm)) +Declaration(Class(:part_of_some_A)) +Declaration(Class(:q_some_C)) +Declaration(Class(:r_some_hand)) +Declaration(Class(:reflexive_part_of_some_arm)) +Declaration(Class(:s_some_Self)) +Declaration(Class(:special_arm)) +Declaration(Class(:special_hand)) +Declaration(Class(:t_some_Self)) +Declaration(ObjectProperty()) +Declaration(ObjectProperty(:p)) +Declaration(ObjectProperty(:part_of)) +Declaration(ObjectProperty(:q)) +Declaration(ObjectProperty(:r)) +Declaration(ObjectProperty(:reflexive_part_of)) +Declaration(ObjectProperty(:s)) +Declaration(ObjectProperty(:t)) +Declaration(NamedIndividual()) +Declaration(NamedIndividual(:a)) +############################ +# Object Properties +############################ + +# Object Property: () + +ObjectPropertyRange( ) + +# Object Property: :p (:p) + +ObjectPropertyRange(:p :C) + +# Object Property: :part_of (:part_of) + +SubObjectPropertyOf(:part_of :reflexive_part_of) +TransitiveObjectProperty(:part_of) +ObjectPropertyRange(:part_of :A) + +# Object Property: :q (:q) + +SubObjectPropertyOf(:q :p) + +# Object Property: :r (:r) + +ObjectPropertyRange(:r :B) + +# Object Property: :reflexive_part_of (:reflexive_part_of) + +ReflexiveObjectProperty(:reflexive_part_of) + +# Object Property: :t (:t) + +SubObjectPropertyOf(:t :s) + + +############################ +# Classes +############################ + +# Class: () + +EquivalentClasses( ObjectSomeValuesFrom( ObjectIntersectionOf( ))) + +# Class: :A (:A) + +SubClassOf(:A ObjectSomeValuesFrom(:q :B)) + +# Class: :D (:D) + +SubClassOf(:D ObjectSomeValuesFrom(:t :D)) + +# Class: :arm (:arm) + +SubClassOf(:arm ObjectSomeValuesFrom(:r :hand)) + +# Class: :finger (:finger) + +SubClassOf(:finger ObjectSomeValuesFrom(:part_of :special_hand)) + +# Class: :hand (:hand) + +SubClassOf(:hand ObjectSomeValuesFrom(:part_of :special_arm)) + +# Class: :part_of_arm (:part_of_arm) + +EquivalentClasses(:part_of_arm ObjectSomeValuesFrom(:part_of :arm)) + +# Class: :part_of_some_A (:part_of_some_A) + +EquivalentClasses(:part_of_some_A ObjectSomeValuesFrom(:part_of :A)) + +# Class: :q_some_C (:q_some_C) + +EquivalentClasses(:q_some_C ObjectSomeValuesFrom(:q :C)) + +# Class: :r_some_hand (:r_some_hand) + +EquivalentClasses(:r_some_hand ObjectSomeValuesFrom(:r :hand)) + +# Class: :reflexive_part_of_some_arm (:reflexive_part_of_some_arm) + +EquivalentClasses(:reflexive_part_of_some_arm ObjectSomeValuesFrom(:reflexive_part_of :arm)) + +# Class: :special_arm (:special_arm) + +SubClassOf(:special_arm :arm) + +# Class: :special_hand (:special_hand) + +SubClassOf(:special_hand :hand) + + +############################ +# Named Individuals +############################ + +# Individual: () + +ClassAssertion(ObjectSomeValuesFrom( ) ) + +# Individual: :a (:a) + +ObjectPropertyAssertion(:t :a :a) + + +) \ No newline at end of file diff --git a/modules/owlapi/src/test/scala/org/geneontology/whelk/TestInferences.scala b/modules/owlapi/src/test/scala/org/geneontology/whelk/TestInferences.scala index 48481c4..bb693e0 100644 --- a/modules/owlapi/src/test/scala/org/geneontology/whelk/TestInferences.scala +++ b/modules/owlapi/src/test/scala/org/geneontology/whelk/TestInferences.scala @@ -31,6 +31,8 @@ object TestInferences extends TestSuite { "taxon-unions.ofn" - compareWhelkAndHermiT(false) "unions.ofn" - compareWhelkAndHermiT(false) "long-chains.ofn" - compareWhelkAndELK() + "part-of-arm-ranges.ofn" - compareWhelkAndHermiT(false) + "part-of-arm-ranges2.ofn" - compareWhelkAndHermiT(false) "owlrl/eq-ref.ofn" - compareWhelkAndHermiT(true) "owlrl/eq-sym.ofn" - compareWhelkAndHermiT(true) "owlrl/eq-trans.ofn" - compareWhelkAndHermiT(true)