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 4407718..8074d8e 100644
--- a/modules/core/src/main/scala/org/geneontology/whelk/Model.scala
+++ b/modules/core/src/main/scala/org/geneontology/whelk/Model.scala
@@ -74,6 +74,10 @@ object BuiltIn {
final val Bottom = AtomicConcept(s"$owl#Nothing")
+ final val SameAs = Role(s"$owl#sameAs")
+
+ final val DifferentFrom = Role(s"$owl#differentFrom")
+
}
final case class Conjunction(left: Concept, right: Concept) extends Concept {
@@ -112,6 +116,30 @@ final case class ExistentialRestriction(role: Role, concept: Concept) extends Co
}
+final case class UniversalRestriction(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)
+
+}
+
+final case class MaxCardinalityRestriction(role: Role, concept: Concept, cardinality: Int) 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)
+
+}
+
final case class SelfRestriction(role: Role) extends Concept {
def conceptSignature: Set[Concept] = Set(this)
@@ -260,6 +288,22 @@ final case class RoleAtom(predicate: Role, subject: IndividualArgument, target:
}
+final case class SameIndividualsAtom(left: IndividualArgument, right: IndividualArgument) extends RuleAtom {
+
+ def signature: Set[Entity] = left.signature ++ right.signature
+
+ def variables: Set[Variable] = Set(left, right).collect { case v: Variable => v }
+
+}
+
+final case class DifferentIndividualsAtom(left: IndividualArgument, right: IndividualArgument) extends RuleAtom {
+
+ def signature: Set[Entity] = left.signature ++ right.signature
+
+ def variables: Set[Variable] = Set(left, right).collect { case v: Variable => v }
+
+}
+
final case class Rule(body: List[RuleAtom], head: List[RuleAtom]) extends Axiom {
def signature: Set[Entity] = body.flatMap(_.signature).toSet ++ head.flatMap(_.signature).toSet
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 2362592..38cf3fd 100644
--- a/modules/core/src/main/scala/org/geneontology/whelk/Reasoner.scala
+++ b/modules/core/src/main/scala/org/geneontology/whelk/Reasoner.scala
@@ -26,6 +26,9 @@ final case class ReasonerState(
negExistsMapByConcept: Map[Concept, Set[ExistentialRestriction]] = Map.empty,
propagations: Map[Concept, Map[Role, List[ExistentialRestriction]]] = Map.empty,
assertedNegativeSelfRestrictionsByRole: Map[Role, SelfRestriction] = Map.empty,
+ universalRestrictionTypesByRole: Map[Nominal, Map[Role, List[Concept]]] = Map.empty,
+ maxOneCardinalityRestrictionTypesByRole: Map[Nominal, Map[Role, List[Concept]]] = Map.empty,
+ maxOneCardinalityRestrictionTypesByFiller: Map[Concept, Map[Role, List[Nominal]]] = Map.empty,
ruleEngine: RuleEngine = RuleEngine.empty,
wm: WorkingMemory = RuleEngine.empty.emptyMemory,
disableBottom: Boolean = false,
@@ -123,7 +126,7 @@ 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 rules = axioms.collect { case r: Rule => r } //TODO create rules from certain Whelk axioms
+ val rules = axioms.collect { case r: Rule => r }
val anonymousRulePredicates = rules.flatMap(_.body.collect {
case ConceptAtom(concept, _) if concept.isAnonymous => ConceptInclusion(concept, Top)
})
@@ -134,10 +137,17 @@ object Reasoner {
}
def assert(axioms: Set[ConceptInclusion], reasoner: ReasonerState): ReasonerState = {
+ val ruleConcepts = reasoner.ruleEngine.rules.flatMap(_.signature).collect {
+ case ac: AtomicConcept => ac
+ case i: Individual => Nominal(i)
+ }
val distinctConcepts = axioms.flatMap {
case ConceptInclusion(subclass, superclass) => Set(subclass, superclass)
}.flatMap(_.conceptSignature)
- val atomicConcepts = distinctConcepts.collect { case a: AtomicConcept => a }
+ val conceptsToQueue = distinctConcepts.collect {
+ case a: AtomicConcept => a
+ case n: Nominal => n
+ }
val additionalAxioms = distinctConcepts.flatMap {
case d @ Disjunction(_) => `R⊔`(d)
case c @ Complement(_) => `R¬`(c, reasoner.disableBottom)
@@ -145,7 +155,7 @@ object Reasoner {
}
val negativeSelfRestrictions = axioms.flatMap(_.subclass.conceptSignature).collect { case sr: SelfRestriction => sr.role -> sr }.toMap
val updatedAssertions = additionalAxioms.toList ::: axioms.toList
- val todo = mutable.Stack.from[QueueExpression](atomicConcepts.toList ::: updatedAssertions)
+ val todo = mutable.Stack.from[QueueExpression](ruleConcepts.toList ::: conceptsToQueue.toList ::: updatedAssertions)
computeClosure(reasoner.copy(
assertions = reasoner.assertions ::: updatedAssertions,
assertedNegativeSelfRestrictionsByRole = negativeSelfRestrictions),
@@ -199,10 +209,12 @@ object Reasoner {
val closureSubsBySuperclass = reasoner.closureSubsBySuperclass.updated(superclass, subs + subclass)
val supers = reasoner.closureSubsBySubclass.getOrElse(subclass, Set.empty)
val closureSubsBySubclass = reasoner.closureSubsBySubclass.updated(subclass, supers + superclass)
- val updatedReasoner = `R⊔right`(ci, `R+⟲`(ci, `R-⟲`(ci, `R⊑right`(ci, `R+∃b-right`(ci, `R-∃`(ci, `R+⨅left`(ci, `R+⨅right`(ci, `R-⨅`(ci, `R⊥left`(ci, reasoner.copy(closureSubsBySuperclass = closureSubsBySuperclass, closureSubsBySubclass = closureSubsBySubclass), todo), todo), todo), todo), todo), todo), todo), todo), todo))
+ val updatedReasoner = `R≤1-c`(ci, `R≤1-a`(ci, `R∀-left`(ci, `R∃ind-left`(ci, `R⊔right`(ci, `R+⟲`(ci, `R-⟲`(ci, `R⊑right`(ci, `R+∃b-right`(ci, `R-∃`(ci, `R+⨅left`(ci, `R+⨅right`(ci, `R-⨅`(ci, `R⊥left`(ci, reasoner.copy(closureSubsBySuperclass = closureSubsBySuperclass, closureSubsBySubclass = closureSubsBySubclass), todo), todo), todo), todo), todo), todo), todo), todo), todo)), todo), todo), todo), todo)
val newState = ci match {
- case ConceptInclusion(Nominal(ind), concept) => reasoner.ruleEngine.processConceptAssertion(ConceptAssertion(concept, ind), updatedReasoner, todo)
- case _ => updatedReasoner
+ case ConceptInclusion(Nominal(left), Nominal(right)) =>
+ reasoner.ruleEngine.processRoleAssertion(RoleAssertion(BuiltIn.SameAs, left, right), updatedReasoner, todo)
+ case ConceptInclusion(Nominal(ind), concept) => reasoner.ruleEngine.processConceptAssertion(ConceptAssertion(concept, ind), updatedReasoner, todo)
+ case _ => updatedReasoner
}
newState.queueDelegates.keysIterator.foldLeft(newState) { (state, delegateKey) =>
state.queueDelegates(delegateKey).processConceptInclusion(ci, state)
@@ -218,10 +230,11 @@ object Reasoner {
val closureSubsBySuperclass = reasoner.closureSubsBySuperclass.updated(superclass, subs + subclass)
val supers = reasoner.closureSubsBySubclass.getOrElse(subclass, Set.empty)
val closureSubsBySubclass = reasoner.closureSubsBySubclass.updated(subclass, supers + superclass)
- val updatedReasoner = `R⊔right`(ci, `R-⟲`(ci, `R⊑right`(ci, `R+∃b-right`(ci, `R+⨅left`(ci, `R+⨅right`(ci, `R⊥left`(ci, reasoner.copy(closureSubsBySuperclass = closureSubsBySuperclass, closureSubsBySubclass = closureSubsBySubclass), todo), todo), todo), todo), todo), todo))
+ val updatedReasoner = `R≤1-c`(ci, `R≤1-a`(ci, `R∀-left`(ci, `R∃ind-left`(ci, `R⊔right`(ci, `R-⟲`(ci, `R⊑right`(ci, `R+∃b-right`(ci, `R+⨅left`(ci, `R+⨅right`(ci, `R⊥left`(ci, reasoner.copy(closureSubsBySuperclass = closureSubsBySuperclass, closureSubsBySubclass = closureSubsBySubclass), todo), todo), todo), todo), todo), todo)), todo), todo), todo), todo)
val newState = ci match {
- case ConceptInclusion(Nominal(ind), concept) => updatedReasoner.ruleEngine.processConceptAssertion(ConceptAssertion(concept, ind), updatedReasoner, todo)
- case _ => updatedReasoner
+ case ConceptInclusion(Nominal(left), Nominal(right)) => reasoner.ruleEngine.processRoleAssertion(RoleAssertion(BuiltIn.SameAs, left, right), updatedReasoner, todo)
+ case ConceptInclusion(Nominal(ind), concept) => updatedReasoner.ruleEngine.processConceptAssertion(ConceptAssertion(concept, ind), updatedReasoner, todo)
+ case _ => updatedReasoner
}
newState.queueDelegates.keysIterator.foldLeft(newState) { (state, delegateKey) =>
state.queueDelegates(delegateKey).processSubPlus(ci, state)
@@ -242,7 +255,7 @@ object Reasoner {
val updatedSubjects = subject :: subjects
val updatedRolesToSubjects = rolesToSubjects.updated(role, updatedSubjects)
val linksByTarget = reasoner.linksByTarget.updated(target, updatedRolesToSubjects)
- val updatedReasoner = `R+⟲𝒪`(link, `R⤳`(link, `R∘left`(link, `R∘right`(link, `R+∃right`(link, `R⊥right`(link, reasoner.copy(linksBySubject = linksBySubject, linksByTarget = linksByTarget), todo), todo), todo), todo), todo), todo)
+ val updatedReasoner = `R≤1-b`(link, `R∀-right`(link, `R∃ind-right`(link, `R+⟲𝒪`(link, `R⤳`(link, `R∘left`(link, `R∘right`(link, `R+∃right`(link, `R⊥right`(link, reasoner.copy(linksBySubject = linksBySubject, linksByTarget = linksByTarget), todo), todo), todo), todo), todo), todo), todo), todo), todo)
val newState = link match {
case Link(Nominal(subjectInd), aRole, Nominal(targetInd)) => updatedReasoner.ruleEngine.processRoleAssertion(RoleAssertion(aRole, subjectInd, targetInd), updatedReasoner, todo)
case _ => updatedReasoner
@@ -383,7 +396,7 @@ object Reasoner {
// better for GO
for {
(right, conjunction) <- conjunctionsMatchingLeft
- if (d2s(right))
+ if d2s(right)
} todo.push(`Sub+`(ConceptInclusion(c, conjunction)))
}
reasoner
@@ -403,7 +416,7 @@ object Reasoner {
} else {
for {
(left, conjunction) <- conjunctionsMatchingRight
- if (d1s(left))
+ if d1s(left)
} todo.push(`Sub+`(ConceptInclusion(c, conjunction)))
}
reasoner
@@ -474,6 +487,154 @@ object Reasoner {
reasoner
}
+ // Implementation of OWL RL eq-rep-o
+ private[this] def `R∃ind-left`(ci: ConceptInclusion, reasoner: ReasonerState, todo: mutable.Stack[QueueExpression]): ReasonerState = {
+ ci match {
+ case ConceptInclusion(subclass: Nominal, superclass: Nominal) =>
+ for {
+ (r, subjects) <- reasoner.linksByTarget.getOrElse(superclass, Map.empty)
+ s <- subjects
+ } todo.push(Link(s, r, subclass))
+ case _ => ()
+ }
+ reasoner
+ }
+
+ // Implementation of OWL RL eq-rep-o
+ private[this] def `R∃ind-right`(link: Link, reasoner: ReasonerState, todo: mutable.Stack[QueueExpression]): ReasonerState = {
+ link match {
+ case Link(s: Nominal, r, o: Nominal) =>
+ for {
+ same <- reasoner.closureSubsBySuperclass.getOrElse(o, Set.empty).collect {
+ case n: Nominal => n
+ }
+ } todo.push(Link(s, r, same))
+ case _ => ()
+ }
+ reasoner
+ }
+
+ // Implementation of OWL RL cls-avf
+ private[this] def `R∀-left`(ci: ConceptInclusion, reasoner: ReasonerState, todo: mutable.Stack[QueueExpression]): ReasonerState = {
+ val updatedRestrictionTypesByRole = ci match {
+ case ConceptInclusion(n: Nominal, UniversalRestriction(role, filler)) =>
+ for {
+ target @ Nominal(_) <- reasoner.linksBySubject.getOrElse(n, Map.empty).getOrElse(role, Set.empty)
+ } todo.push(ConceptInclusion(target, filler))
+ reasoner.universalRestrictionTypesByRole.updatedWith(n) {
+ case Some(rolesToFillers) => Some(rolesToFillers.updatedWith(role) {
+ case Some(fillers) => Some(filler :: fillers)
+ case None => Some(List(filler))
+ })
+ case None => Some(Map(role -> List(filler)))
+ }
+ case _ => reasoner.universalRestrictionTypesByRole
+ }
+ reasoner.copy(universalRestrictionTypesByRole = updatedRestrictionTypesByRole)
+ }
+
+ // Implementation of OWL RL cls-avf
+ private[this] def `R∀-right`(link: Link, reasoner: ReasonerState, todo: mutable.Stack[QueueExpression]): ReasonerState = {
+ link match {
+ case Link(s: Nominal, role, o: Nominal) =>
+ for {
+ filler <- reasoner.universalRestrictionTypesByRole.getOrElse(s, Map.empty).getOrElse(role, Nil)
+ } todo.push(ConceptInclusion(o, filler))
+ case _ => ()
+ }
+ reasoner
+ }
+
+ // Max 1 restrictions rules may need some optimization to reduce repeated joins
+ // x ⊑ ≤1R.C ∧ x R y ∧ x R z ∧ y ⊑ C ∧ z ⊑ C
+ // assuming all role subsumptions get materialized for individuals
+ // this rule: x ⊑ ≤1R.C
+ private[this] def `R≤1-a`(ci: ConceptInclusion, reasoner: ReasonerState, todo: mutable.Stack[QueueExpression]): ReasonerState =
+ ci match {
+ case ConceptInclusion(subject: Nominal, MaxCardinalityRestriction(role, filler, cardinality))
+ if cardinality == 1 =>
+ val fillerSubclasses = reasoner.closureSubsBySuperclass.getOrElse(filler, Set.empty)
+ val targets = reasoner.linksBySubject.getOrElse(subject, Map.empty).getOrElse(role, Set.empty)
+ .collect {
+ case n: Nominal => n
+ }
+ .filter(fillerSubclasses)
+ if (targets.size > 1) {
+ for {
+ pair <- targets.toList.combinations(2)
+ } todo.push(ConceptInclusion(pair(0), pair(1)), ConceptInclusion(pair(1), pair(0)))
+ }
+ val updatedRestrictionTypesByRole = reasoner.maxOneCardinalityRestrictionTypesByRole.updatedWith(subject) {
+ case Some(rolesToFillers) => Some(rolesToFillers.updatedWith(role) {
+ case Some(fillers) => Some(filler :: fillers)
+ case None => Some(List(filler))
+ })
+ case None => Some(Map(role -> List(filler)))
+ }
+ val updatedRestrictionTypesByFiller = reasoner.maxOneCardinalityRestrictionTypesByFiller.updatedWith(filler) {
+ case Some(rolesToInstances) => Some(rolesToInstances.updatedWith(role) {
+ case Some(instances) => Some(subject :: instances)
+ case None => Some(List(subject))
+ })
+ case None => Some(Map(role -> List(subject)))
+ }
+ reasoner.copy(maxOneCardinalityRestrictionTypesByRole = updatedRestrictionTypesByRole,
+ maxOneCardinalityRestrictionTypesByFiller = updatedRestrictionTypesByFiller)
+
+ case _ => reasoner
+ }
+
+ // x ⊑ ≤1R.C ∧ x R y ∧ x R z ∧ y ⊑ C ∧ z ⊑ C
+ // this rule: x R y or x R z
+ private[this] def `R≤1-b`(link: Link, reasoner: ReasonerState, todo: mutable.Stack[QueueExpression]): ReasonerState = {
+ link match {
+ case Link(s: Nominal, role, o: Nominal) =>
+ val fillers = reasoner.maxOneCardinalityRestrictionTypesByRole.getOrElse(s, Map.empty).getOrElse(role, Nil)
+ if (fillers.nonEmpty) {
+ val targets = reasoner.linksBySubject.getOrElse(s, Map.empty).getOrElse(role, Set.empty)
+ .collect {
+ case n: Nominal => n
+ }
+ for {
+ filler <- fillers
+ fillerSubclasses = reasoner.closureSubsBySuperclass.getOrElse(filler, Set.empty)
+ target <- targets.iterator.filter(fillerSubclasses)
+ if target != o
+ } {
+ todo.push(ConceptInclusion(o, target))
+ todo.push(ConceptInclusion(target, o))
+ }
+ }
+ case _ => ()
+ }
+ reasoner
+ }
+
+ // x ⊑ ≤1R.C ∧ x R y ∧ x R z ∧ y ⊑ C ∧ z ⊑ C
+ // this rule: y ⊑ C or z ⊑ C
+ private[this] def `R≤1-c`(ci: ConceptInclusion, reasoner: ReasonerState, todo: mutable.Stack[QueueExpression]): ReasonerState = {
+ ci match {
+ case ConceptInclusion(target: Nominal, filler) =>
+ val restrictionInstancesByRole = reasoner.maxOneCardinalityRestrictionTypesByFiller.getOrElse(filler, Map.empty)
+ if (restrictionInstancesByRole.nonEmpty) {
+ for {
+ (role, subjects) <- restrictionInstancesByRole
+ subject <- subjects
+ otherTarget <- reasoner.linksBySubject.getOrElse(subject, Map.empty).getOrElse(role, Set.empty).iterator
+ .collect {
+ case n: Nominal => n
+ }
+ if otherTarget != target
+ } {
+ todo.push(ConceptInclusion(otherTarget, target))
+ todo.push(ConceptInclusion(target, otherTarget))
+ }
+ }
+ case _ => ()
+ }
+ reasoner
+ }
+
private[this] def `R-⟲`(ci: ConceptInclusion, reasoner: ReasonerState, todo: mutable.Stack[QueueExpression]): ReasonerState = ci match {
case ConceptInclusion(sub, SelfRestriction(role)) =>
todo.push(Link(sub, role, sub))
diff --git a/modules/core/src/main/scala/org/geneontology/whelk/ReteNodes.scala b/modules/core/src/main/scala/org/geneontology/whelk/ReteNodes.scala
index e84313c..1d12798 100644
--- a/modules/core/src/main/scala/org/geneontology/whelk/ReteNodes.scala
+++ b/modules/core/src/main/scala/org/geneontology/whelk/ReteNodes.scala
@@ -187,8 +187,18 @@ final case class RoleAtomJoinNode(atom: RoleAtom, children: List[BetaNode], spec
if ((individualSubject == assertion.subject) && (individualTarget == assertion.target)) parentMem.tokens
else Nil
}
- //FIXME missing ind with unbound var? both directions
- case (_, _) => (assertion: RoleAssertion, parentMem: BetaMemory) => parentMem.tokens.map(t => t.extend(makeBindings(assertion)))
+ case (individualSubject: Individual, _: Variable) =>
+ (assertion: RoleAssertion, parentMem: BetaMemory) => {
+ if (individualSubject == assertion.subject) parentMem.tokens.map(t => t.extend(makeBindings(assertion)))
+ else Nil
+ }
+ case (_: Variable, individualTarget: Individual) =>
+ (assertion: RoleAssertion, parentMem: BetaMemory) => {
+ if (individualTarget == assertion.target) parentMem.tokens.map(t => t.extend(makeBindings(assertion)))
+ else Nil
+ }
+ case (_, _) => (assertion: RoleAssertion, parentMem: BetaMemory) =>
+ parentMem.tokens.map(t => t.extend(makeBindings(assertion)))
}
@@ -270,10 +280,21 @@ final case class ProductionNode(rule: Rule) extends BetaNode {
atom <- rule.head
} {
atom match {
- case RoleAtom(role, subj, obj) =>
+ case RoleAtom(BuiltIn.SameAs, subj, obj) =>
+ todo.push(ConceptInclusion(Nominal(fillVariable(subj, token)), Nominal(fillVariable(obj, token))))
+ todo.push(ConceptInclusion(Nominal(fillVariable(obj, token)), Nominal(fillVariable(subj, token))))
+ case RoleAtom(BuiltIn.DifferentFrom, subj, obj) =>
+ todo.push(ConceptInclusion(Conjunction(Nominal(fillVariable(subj, token)), Nominal(fillVariable(obj, token))), BuiltIn.Bottom))
+ case RoleAtom(role, subj, obj) =>
todo.push(Link(Nominal(fillVariable(subj, token)), role, Nominal(fillVariable(obj, token))))
- case ConceptAtom(concept, arg) =>
+ case ConceptAtom(concept, arg) =>
todo.push(ConceptInclusion(Nominal(fillVariable(arg, token)), concept))
+ case SameIndividualsAtom(_, _) =>
+ ???
+ // Should never happen; SameIndividualsAtom should be internally handled as RoleAtom
+ case DifferentIndividualsAtom(_, _) =>
+ ???
+ // Should never happen; SameIndividualsAtom should be internally handled as RoleAtom
}
}
reasoner
diff --git a/modules/core/src/main/scala/org/geneontology/whelk/RuleEngine.scala b/modules/core/src/main/scala/org/geneontology/whelk/RuleEngine.scala
index 925e816..2c9ceb1 100644
--- a/modules/core/src/main/scala/org/geneontology/whelk/RuleEngine.scala
+++ b/modules/core/src/main/scala/org/geneontology/whelk/RuleEngine.scala
@@ -4,9 +4,16 @@ import scala.collection.mutable
final case class RuleEngine(rules: Set[Rule]) {
+ val equalityRules = Set(
+ Rule(
+ body = List(RoleAtom(BuiltIn.SameAs, Variable("x"), Variable("y"))),
+ head = List(RoleAtom(BuiltIn.SameAs, Variable("y"), Variable("x")))
+ )
+ )
+
val (conceptAlphaIndex: Map[Concept, ConceptAtomAlphaNode],
roleAlphaIndex: Map[Role, RoleAtomAlphaNode],
- allJoinSpecs: Set[JoinNodeSpec]) = constructReteNetwork(rules)
+ allJoinSpecs: Set[JoinNodeSpec]) = constructReteNetwork(rules ++ equalityRules)
def emptyMemory: WorkingMemory = WorkingMemory(
conceptAlphaIndex.keys.map(c => c -> ConceptAlphaMemory.empty).toMap,
@@ -17,11 +24,26 @@ final case class RuleEngine(rules: Set[Rule]) {
def processConceptAssertion(assertion: ConceptAssertion, reasoner: ReasonerState, todo: mutable.Stack[QueueExpression]): ReasonerState =
conceptAlphaIndex.get(assertion.concept).map(node => node.activate(assertion.individual, reasoner, todo)).getOrElse(reasoner)
- def processRoleAssertion(assertion: RoleAssertion, reasoner: ReasonerState, todo: mutable.Stack[QueueExpression]): ReasonerState =
+ def processRoleAssertion(assertion: RoleAssertion, reasoner: ReasonerState, todo: mutable.Stack[QueueExpression]): ReasonerState = {
roleAlphaIndex.get(assertion.role).map(node => node.activate(assertion, reasoner, todo)).getOrElse(reasoner)
+ }
private def constructReteNetwork(rules: Set[Rule]): (Map[Concept, ConceptAtomAlphaNode], Map[Role, RoleAtomAlphaNode], Set[JoinNodeSpec]) = {
- val nodes = rules.foldLeft(Set.empty[BetaNode]) { (nodes, rule) =>
+ // We handle sameAs and differentFrom using role atom join nodes
+ val normalized = rules.map { rule =>
+ Rule(
+ rule.body.map {
+ case SameIndividualsAtom(left, right) => RoleAtom(BuiltIn.SameAs, left, right)
+ case DifferentIndividualsAtom(left, right) => RoleAtom(BuiltIn.DifferentFrom, left, right)
+ case other => other
+ },
+ rule.head.map {
+ case SameIndividualsAtom(left, right) => RoleAtom(BuiltIn.SameAs, left, right)
+ case DifferentIndividualsAtom(left, right) => RoleAtom(BuiltIn.DifferentFrom, left, right)
+ case other => other
+ })
+ }
+ val nodes = normalized.foldLeft(Set.empty[BetaNode]) { (nodes, rule) =>
val (_, allConceptNodes, allRoleNodes) = processRuleAtoms(rule.body, Nil, rule, Map.empty, Map.empty)
nodes ++ allConceptNodes.values ++ allRoleNodes.values
}
@@ -48,14 +70,20 @@ final case class RuleEngine(rules: Set[Rule]) {
val spec = JoinNodeSpec(thisPatternSequence)
val (child, updatedExistingC, updatedExistingR) = processRuleAtoms(rest, thisPatternSequence, rule, existingC, existingR)
atom match {
- case ca: ConceptAtom =>
+ case ca: ConceptAtom =>
val node = existingC.getOrElse(spec, ConceptAtomJoinNode(ca, Nil, spec))
val updatedNode = node.copy(children = child :: node.children)
(updatedNode, updatedExistingC.updated(spec, updatedNode), updatedExistingR)
- case ra: RoleAtom =>
+ case ra: RoleAtom =>
val node = existingR.getOrElse(spec, RoleAtomJoinNode(ra, Nil, spec))
val updatedNode = node.copy(children = child :: node.children)
(updatedNode, updatedExistingC, updatedExistingR.updated(spec, updatedNode))
+ case _: SameIndividualsAtom =>
+ // Should never happen; SameIndividualsAtom should be internally handled as RoleAtom
+ ???
+ case _: DifferentIndividualsAtom =>
+ // Should never happen; SameIndividualsAtom should be internally handled as RoleAtom
+ ???
}
case Nil => (ProductionNode(rule), existingC, existingR)
}
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 6eaa8bc..ac2236d 100644
--- a/modules/owlapi/src/main/scala/org/geneontology/whelk/Bridge.scala
+++ b/modules/owlapi/src/main/scala/org/geneontology/whelk/Bridge.scala
@@ -32,7 +32,7 @@ object Bridge {
case first :: second :: Nil => Set(ConceptInclusion(first, second), ConceptInclusion(second, first))
case _ => ??? //impossible
}.toSet
- case DisjointClasses(_, operands) if operands.size == 2 => //FIXME handle >2
+ case DisjointClasses(_, operands) =>
val converted = operands.map(convertExpression).toList.collect { case Some(concept) => concept }
converted.combinations(2).flatMap {
case first :: second :: Nil => Set(ConceptInclusion(Conjunction(first, second), Bottom))
@@ -45,9 +45,18 @@ object Bridge {
case ObjectPropertyAssertion(_, ObjectInverseOf(ObjectProperty(prop)), NamedIndividual(obj), NamedIndividual(subj)) =>
Set(ConceptInclusion(Nominal(WIndividual(subj.toString)), ExistentialRestriction(Role(prop.toString), Nominal(WIndividual(obj.toString)))))
case EquivalentObjectProperties(_, propertyExpressions) =>
+ //FIXME handle inverse property expression?
val properties = propertyExpressions.collect { case p @ ObjectProperty(_) => p }.toList
properties.combinations(2).flatMap {
- case ObjectProperty(first) :: ObjectProperty(second) :: Nil => Set(RoleInclusion(Role(first.toString), Role(second.toString)))
+ case ObjectProperty(first) :: ObjectProperty(second) :: Nil =>
+ val role1 = Role(first.toString)
+ val role2 = Role(second.toString)
+ Set(
+ RoleInclusion(role1, role2),
+ RoleInclusion(role2, role1),
+ Rule(body = List(RoleAtom(role1, WVariable("x"), WVariable("y"))), head = List(RoleAtom(role2, WVariable("x"), WVariable("y")))),
+ Rule(body = List(RoleAtom(role2, WVariable("x"), WVariable("y"))), head = List(RoleAtom(role1, WVariable("x"), WVariable("y"))))
+ )
case _ => ??? //impossible
}.toSet
case SubObjectPropertyOf(_, ObjectProperty(subproperty), ObjectProperty(superproperty)) =>
@@ -75,6 +84,22 @@ object Bridge {
val compositionProperty: OWLObjectProperty = ObjectProperty(s"$CompositionRolePrefix${first.getIRI}${second.getIRI}")
convertAxiom(SubObjectPropertyChainOf(List(first, second), compositionProperty)) ++
convertAxiom(SubObjectPropertyChainOf(compositionProperty :: more, superproperty))
+ case DisjointObjectProperties(_, properties) =>
+ properties.toList.combinations(2).flatMap {
+ case first :: second :: Nil =>
+ val firstRoleAtom = first match {
+ case ObjectProperty(iri) => RoleAtom(Role(iri.toString), WVariable("x"), WVariable("y"))
+ case ObjectInverseOf(ObjectProperty(iri)) => RoleAtom(Role(iri.toString), WVariable("y"), WVariable("x"))
+ }
+ val secondRoleAtom = second match {
+ case ObjectProperty(iri) => RoleAtom(Role(iri.toString), WVariable("x"), WVariable("y"))
+ case ObjectInverseOf(ObjectProperty(iri)) => RoleAtom(Role(iri.toString), WVariable("y"), WVariable("x"))
+ }
+ Set(
+ Rule(body = List(firstRoleAtom, secondRoleAtom), head = List(ConceptAtom(Bottom, WVariable("x")), ConceptAtom(Bottom, WVariable("y"))))
+ )
+ case _ => ??? //impossible
+ }.toSet
case TransitiveObjectProperty(_, ObjectProperty(property)) =>
val role = Role(property.toString)
Set(
@@ -83,6 +108,31 @@ object Bridge {
case ReflexiveObjectProperty(_, ObjectProperty(property)) => Set(
ConceptInclusion(Top, SelfRestriction(Role(property.toString)))
)
+ case FunctionalObjectProperty(_, ObjectProperty(property)) =>
+ val role = Role(property.toString)
+ Set(
+ Rule(body = List(RoleAtom(role, WVariable("x"), WVariable("y1")), RoleAtom(role, WVariable("x"), WVariable("y2"))), head = List(SameIndividualsAtom(WVariable("y1"), WVariable("y2"))))
+ )
+ case InverseFunctionalObjectProperty(_, ObjectProperty(property)) =>
+ val role = Role(property.toString)
+ Set(
+ Rule(body = List(RoleAtom(role, WVariable("x1"), WVariable("y")), RoleAtom(role, WVariable("x2"), WVariable("y"))), head = List(SameIndividualsAtom(WVariable("x1"), WVariable("x2"))))
+ )
+ case IrreflexiveObjectProperty(_, ObjectProperty(property)) =>
+ val role = Role(property.toString)
+ Set(
+ Rule(body = List(RoleAtom(role, WVariable("x"), WVariable("x"))), head = List(ConceptAtom(BuiltIn.Bottom, WVariable("x"))))
+ )
+ case SymmetricObjectProperty(_, ObjectProperty(property)) =>
+ val role = Role(property.toString)
+ Set(
+ Rule(body = List(RoleAtom(role, WVariable("x"), WVariable("y"))), head = List(RoleAtom(role, WVariable("y"), WVariable("x"))))
+ )
+ case AsymmetricObjectProperty(_, ObjectProperty(property)) =>
+ val role = Role(property.toString)
+ 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 =>
@@ -94,6 +144,27 @@ object Bridge {
Set(
Rule(body = List(RoleAtom(roleP, x1, x2)), head = List(RoleAtom(roleQ, x2, x1))),
Rule(body = List(RoleAtom(roleQ, x1, x2)), head = List(RoleAtom(roleP, x2, x1))))
+ case NegativeObjectPropertyAssertion(_, ObjectProperty(p), NamedIndividual(x), NamedIndividual(y)) =>
+ val xInd = WIndividual(x.toString)
+ val yInd = WIndividual(y.toString)
+ Set(
+ Rule(body = List(RoleAtom(Role(p.toString), xInd, yInd)), head = List(ConceptAtom(Bottom, xInd), ConceptAtom(Bottom, yInd)))
+ )
+ case SameIndividual(_, individuals) =>
+ individuals.toList.combinations(2).flatMap {
+ case NamedIndividual(first) :: NamedIndividual(second) :: Nil => Set(
+ ConceptInclusion(Nominal(WIndividual(first.toString)), Nominal(WIndividual(second.toString))),
+ ConceptInclusion(Nominal(WIndividual(second.toString)), Nominal(WIndividual(first.toString)))
+ )
+ case _ => ??? //impossible
+ }.toSet
+ case DifferentIndividuals(_, individuals) =>
+ individuals.toList.combinations(2).flatMap {
+ case NamedIndividual(first) :: NamedIndividual(second) :: Nil => Set(
+ ConceptInclusion(Conjunction(Nominal(WIndividual(first.toString)), Nominal(WIndividual(second.toString))), Bottom),
+ )
+ case _ => ??? //impossible
+ }.toSet
case DLSafeRule(_, body, head) => (for {
bodyAtoms <- convertAtomSet(body)
headAtoms <- convertAtomSet(head)
@@ -103,13 +174,15 @@ object Bridge {
Set.empty
}
- //TODO ObjectOneOf
def convertExpression(expression: OWLClassExpression): Option[Concept] = {
expression match {
case OWLThing => Some(Top)
case OWLNothing => Some(Bottom)
case Class(iri) => Some(AtomicConcept(iri.toString))
case ObjectSomeValuesFrom(ObjectProperty(prop), filler) => convertExpression(filler).map(ExistentialRestriction(Role(prop.toString), _))
+ case ObjectAllValuesFrom(ObjectProperty(prop), filler) => convertExpression(filler).map(UniversalRestriction(Role(prop.toString), _))
+ case ObjectMaxCardinality(0, ObjectProperty(prop), filler) => convertExpression(filler).map(c => Complement(ExistentialRestriction(Role(prop.toString), c)))
+ case ObjectMaxCardinality(1, ObjectProperty(prop), filler) => convertExpression(filler).map(MaxCardinalityRestriction(Role(prop.toString), _, 1))
case ObjectHasSelf(ObjectProperty(prop)) => Some(SelfRestriction(Role(prop.toString)))
case ObjectIntersectionOf(operands) if operands.nonEmpty =>
def convert(items: List[Concept]): Concept = items match {
@@ -122,7 +195,13 @@ object Bridge {
case ObjectUnionOf(operands) =>
operands.toList.map(convertExpression).sequence.map(_.toSet).map(Disjunction)
case ObjectComplementOf(concept) => convertExpression(concept).map(Complement)
- case ObjectOneOf(individuals) if individuals.size == 1 => individuals.collectFirst { case NamedIndividual(iri) => Nominal(WIndividual(iri.toString)) }
+ case ObjectOneOf(individuals) =>
+ val operands = individuals.collect {
+ case NamedIndividual(iri) => Nominal(WIndividual(iri.toString))
+ }
+ if (operands.isEmpty) None
+ else if (operands.size == 1) operands.headOption
+ else Some(Disjunction(operands.toSet[Concept]))
case ObjectHasValue(ObjectProperty(prop), NamedIndividual(ind)) => Some(ExistentialRestriction(Role(prop.toString), Nominal(WIndividual(ind.toString))))
case DataSomeValuesFrom(DataProperty(prop), range) => Some(DataRestriction(DataRole(prop.toString), DataRange(range)))
//scowl is missing DataHasValue
@@ -145,7 +224,16 @@ object Bridge {
subject <- convertAtomArg(subj)
target <- convertAtomArg(obj)
} yield RoleAtom(Role(iri.toString), subject, target)
- case ObjectPropertyAtom(ObjectInverseOf(prop @ ObjectProperty(_)), subj, obj) => convertRuleAtom(ObjectPropertyAtom(prop, obj, subj))
+ case ObjectPropertyAtom(ObjectInverseOf(prop @ ObjectProperty(_)), subj, obj) =>
+ convertRuleAtom(ObjectPropertyAtom(prop, obj, subj))
+ case swrl.SameIndividualAtom(left, right) => for {
+ subject <- convertAtomArg(left)
+ target <- convertAtomArg(right)
+ } yield RoleAtom(SameAs, subject, target)
+ case swrl.DifferentIndividualsAtom(left, right) => for {
+ subject <- convertAtomArg(left)
+ target <- convertAtomArg(right)
+ } yield RoleAtom(DifferentFrom, subject, target)
case _ => None
}
diff --git a/modules/owlapi/src/test/resources/org/geneontology/whelk/incremental-allvaluesfrom.ofn b/modules/owlapi/src/test/resources/org/geneontology/whelk/incremental-allvaluesfrom.ofn
new file mode 100644
index 0000000..e90729c
--- /dev/null
+++ b/modules/owlapi/src/test/resources/org/geneontology/whelk/incremental-allvaluesfrom.ofn
@@ -0,0 +1,42 @@
+Prefix(:=)
+Prefix(owl:=)
+Prefix(rdf:=)
+Prefix(xml:=)
+Prefix(xsd:=)
+Prefix(rdfs:=)
+
+
+Ontology(
+
+Declaration(Class())
+Declaration(Class())
+Declaration(Class())
+Declaration(Class())
+Declaration(ObjectProperty())
+Declaration(NamedIndividual())
+Declaration(NamedIndividual())
+
+############################
+# Classes
+############################
+
+# Class: ()
+
+EquivalentClasses( ObjectAllValuesFrom( ))
+
+# Class: ()
+
+SubClassOf( )
+
+
+############################
+# Named Individuals
+############################
+
+# Individual: ()
+
+ClassAssertion( )
+ObjectPropertyAssertion( )
+
+
+)
\ No newline at end of file
diff --git a/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cax-dw-adc.ofn b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cax-dw-adc.ofn
new file mode 100644
index 0000000..58e5466
--- /dev/null
+++ b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cax-dw-adc.ofn
@@ -0,0 +1,57 @@
+Prefix(:=)
+Prefix(owl:=)
+Prefix(rdf:=)
+Prefix(xml:=)
+Prefix(xsd:=)
+Prefix(rdfs:=)
+
+
+Ontology(
+
+Declaration(Class())
+Declaration(Class())
+Declaration(Class())
+Declaration(Class())
+Declaration(ObjectProperty())
+Declaration(ObjectProperty())
+Declaration(NamedIndividual())
+Declaration(NamedIndividual())
+############################
+# Object Properties
+############################
+
+# Object Property: ()
+
+SubObjectPropertyOf( )
+
+
+############################
+# Classes
+############################
+
+# Class: ()
+
+
+# Class: ()
+
+
+# Class: ()
+
+
+# Class: ()
+
+SubClassOf( )
+
+
+############################
+# Named Individuals
+############################
+
+# Individual: ()
+
+ClassAssertion( )
+ClassAssertion( )
+
+
+DisjointClasses( )
+)
\ No newline at end of file
diff --git a/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cax-eqc.ofn b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cax-eqc.ofn
new file mode 100644
index 0000000..73784fd
--- /dev/null
+++ b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cax-eqc.ofn
@@ -0,0 +1,45 @@
+Prefix(:=)
+Prefix(owl:=)
+Prefix(rdf:=)
+Prefix(xml:=)
+Prefix(xsd:=)
+Prefix(rdfs:=)
+
+
+Ontology(
+
+Declaration(Class())
+Declaration(Class())
+Declaration(Class())
+Declaration(Class())
+Declaration(ObjectProperty())
+Declaration(NamedIndividual())
+Declaration(NamedIndividual())
+
+############################
+# Classes
+############################
+
+# Class: ()
+
+EquivalentClasses( )
+
+# Class: ()
+
+EquivalentClasses( )
+
+
+############################
+# Named Individuals
+############################
+
+# Individual: ()
+
+ClassAssertion( )
+
+# Individual: ()
+
+ClassAssertion( )
+
+
+)
\ No newline at end of file
diff --git a/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cax-sco.ofn b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cax-sco.ofn
new file mode 100644
index 0000000..9482e84
--- /dev/null
+++ b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cax-sco.ofn
@@ -0,0 +1,39 @@
+Prefix(:=)
+Prefix(owl:=)
+Prefix(rdf:=)
+Prefix(xml:=)
+Prefix(xsd:=)
+Prefix(rdfs:=)
+
+
+Ontology(
+
+Declaration(Class())
+Declaration(Class())
+Declaration(Class())
+Declaration(ObjectProperty())
+Declaration(NamedIndividual())
+
+############################
+# Classes
+############################
+
+# Class: ()
+
+SubClassOf( )
+
+# Class: ()
+
+SubClassOf( )
+
+
+############################
+# Named Individuals
+############################
+
+# Individual: ()
+
+ClassAssertion( )
+
+
+)
\ No newline at end of file
diff --git a/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-avf.ofn b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-avf.ofn
new file mode 100644
index 0000000..6f39a6a
--- /dev/null
+++ b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-avf.ofn
@@ -0,0 +1,36 @@
+Prefix(:=)
+Prefix(owl:=)
+Prefix(rdf:=)
+Prefix(xml:=)
+Prefix(xsd:=)
+Prefix(rdfs:=)
+
+
+Ontology(
+
+Declaration(Class())
+Declaration(Class())
+Declaration(ObjectProperty())
+Declaration(NamedIndividual())
+Declaration(NamedIndividual())
+
+############################
+# Classes
+############################
+
+# Class: ()
+
+EquivalentClasses( ObjectAllValuesFrom( ))
+
+
+############################
+# Named Individuals
+############################
+
+# Individual: ()
+
+ClassAssertion( )
+ObjectPropertyAssertion( )
+
+
+)
\ No newline at end of file
diff --git a/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-com.ofn b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-com.ofn
new file mode 100644
index 0000000..c3eda86
--- /dev/null
+++ b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-com.ofn
@@ -0,0 +1,35 @@
+Prefix(:=)
+Prefix(owl:=)
+Prefix(rdf:=)
+Prefix(xml:=)
+Prefix(xsd:=)
+Prefix(rdfs:=)
+
+
+Ontology(
+
+Declaration(Class())
+Declaration(Class())
+Declaration(ObjectProperty())
+Declaration(NamedIndividual())
+
+############################
+# Classes
+############################
+
+# Class: ()
+
+EquivalentClasses( ObjectComplementOf())
+
+
+############################
+# Named Individuals
+############################
+
+# Individual: ()
+
+ClassAssertion( )
+ClassAssertion( )
+
+
+)
\ No newline at end of file
diff --git a/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-hv.ofn b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-hv.ofn
new file mode 100644
index 0000000..61da4d3
--- /dev/null
+++ b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-hv.ofn
@@ -0,0 +1,39 @@
+Prefix(:=)
+Prefix(owl:=)
+Prefix(rdf:=)
+Prefix(xml:=)
+Prefix(xsd:=)
+Prefix(rdfs:=)
+
+
+Ontology(
+
+Declaration(Class())
+Declaration(ObjectProperty())
+Declaration(NamedIndividual())
+Declaration(NamedIndividual())
+Declaration(NamedIndividual())
+
+############################
+# Classes
+############################
+
+# Class: ()
+
+EquivalentClasses( ObjectHasValue( ))
+
+
+############################
+# Named Individuals
+############################
+
+# Individual: ()
+
+ClassAssertion( )
+
+# Individual: ()
+
+ObjectPropertyAssertion( )
+
+
+)
\ No newline at end of file
diff --git a/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-int.ofn b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-int.ofn
new file mode 100644
index 0000000..88d3a13
--- /dev/null
+++ b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-int.ofn
@@ -0,0 +1,43 @@
+Prefix(:=)
+Prefix(owl:=)
+Prefix(rdf:=)
+Prefix(xml:=)
+Prefix(xsd:=)
+Prefix(rdfs:=)
+
+
+Ontology(
+
+Declaration(Class())
+Declaration(Class())
+Declaration(Class())
+Declaration(Class())
+Declaration(ObjectProperty())
+Declaration(NamedIndividual())
+Declaration(NamedIndividual())
+
+############################
+# Classes
+############################
+
+# Class: ()
+
+EquivalentClasses( ObjectIntersectionOf( ))
+
+
+############################
+# Named Individuals
+############################
+
+# Individual: ()
+
+ClassAssertion( )
+ClassAssertion( )
+ClassAssertion( )
+
+# Individual: ()
+
+ClassAssertion( )
+
+
+)
\ No newline at end of file
diff --git a/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-maxqc0.ofn b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-maxqc0.ofn
new file mode 100644
index 0000000..465accd
--- /dev/null
+++ b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-maxqc0.ofn
@@ -0,0 +1,54 @@
+Prefix(:=)
+Prefix(owl:=)
+Prefix(rdf:=)
+Prefix(xml:=)
+Prefix(xsd:=)
+Prefix(rdfs:=)
+
+
+Ontology(
+
+Declaration(Class())
+Declaration(Class())
+Declaration(Class())
+Declaration(ObjectProperty())
+Declaration(ObjectProperty())
+Declaration(NamedIndividual())
+Declaration(NamedIndividual())
+############################
+# Object Properties
+############################
+
+# Object Property: ()
+
+SubObjectPropertyOf( )
+
+
+############################
+# Classes
+############################
+
+# Class: ()
+
+SubClassOf( ObjectMaxCardinality(0 ))
+
+# Class: ()
+
+SubClassOf( )
+
+
+############################
+# Named Individuals
+############################
+
+# Individual: ()
+
+ClassAssertion( )
+ObjectPropertyAssertion( )
+
+# Individual: ()
+
+ClassAssertion( )
+
+
+)
\ No newline at end of file
diff --git a/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-maxqc1.ofn b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-maxqc1.ofn
new file mode 100644
index 0000000..02cda34
--- /dev/null
+++ b/modules/owlapi/src/test/resources/org/geneontology/whelk/owlrl/cls-maxqc1.ofn
@@ -0,0 +1,48 @@
+Prefix(:=