Wildlife classification -- the powertype pattern: gufo:partitions
"Lion" is both a class (of individual lions) and, arguably, an instance of something -- "a species" is
itself a kind of thing you might want to reason about (how many species are there?). gUFO's higher-order
types let a class like :AnimalSpecies have other classes as its instances, using OWL 2
punning, and let you say precisely how strict the relationship between those instance-classes and their
common base class is: gufo:partitions requires it to be a pairwise-disjoint specialization.
:AnimalSpecies is declared a plain gufo:Type. This
is also the example that leans most on the SHACL shapes, since the rules governing this pattern (unlike a
straightforward class hierarchy) are exactly the kind OWL alone cannot check on its own.
A wildlife classification scheme: animals belong to exactly one species (Lion, Hyena, Elephant), and species -- not individual animals -- are assessed for conservation status (Endangered, Vulnerable, Least concern), IUCN-style: population decline or recovery is a fact about a species as a whole, not about any one animal in it. (Animal names are fictional.)
:Animal a owl:Class , gufo:Kind ;
rdfs:subClassOf gufo:FunctionalComplex .
gufo:partitions: species, strictlyEvery animal belongs to exactly one species, and no animal can belong to two -- a lion is never also a
hyena. This is exactly what gufo:partitions is for: a higher-order type whose instances must be
pairwise disjoint subclasses of the base type.
:AnimalSpecies a owl:Class , gufo:Type ;
rdfs:subClassOf gufo:Type ;
gufo:partitions :Animal .
:Lion a owl:Class , :AnimalSpecies ;
rdfs:subClassOf :Animal .
:Hyena a owl:Class , :AnimalSpecies ;
rdfs:subClassOf :Animal .
:Elephant a owl:Class , :AnimalSpecies ;
rdfs:subClassOf :Animal .
[] a owl:AllDisjointClasses ;
owl:members ( :Lion :Hyena :Elephant ) .
Look closely at what :Lion is doing here: it is declared a owl:Class (so
individual lions can instantiate it), a :AnimalSpecies (so it is, itself, an instance of the
higher-order type -- this is the punning), and rdfs:subClassOf :Animal (so its instances are
animals). Three facts about the same name, at once, and all three are needed for the pattern to mean
anything.
:AnimalSpecies is declared a gufo:Type and rdfs:subClassOf gufo:Type
-- the most general acknowledgment that it is itself a type, and that its instances (:Lion and
so on) are types too.
:AnimalSpeciesPopulation decline or recovery is a fact about a species as a whole, not about any one animal in it -- but
that does not mean it needs powertype machinery of its own. It is simply a property whose domain is
:AnimalSpecies (the very class just declared above), following the same enumerated-value pattern
as :ShirtSize in example 3:
:ConservationStatus a owl:Class ;
owl:equivalentClass [ a owl:Class ; owl:oneOf ( :Endangered :Vulnerable :LeastConcern ) ] .
:hasConservationStatus a owl:ObjectProperty ;
rdfs:domain :AnimalSpecies ;
rdfs:range :ConservationStatus .
:Lion :hasConservationStatus :Endangered .
:Hyena :hasConservationStatus :LeastConcern .
:Elephant :hasConservationStatus :Vulnerable .
:hasConservationStatus applies to :Lion for exactly the same reason any ordinary
property applies to any ordinary individual: :Lion is an instance of :AnimalSpecies,
full stop -- no further punning insight is needed at this step. All the higher-order work already happened
when :Lion was declared an instance of :AnimalSpecies above.
:Kesi a :Lion .
:Nia a :Hyena .
:Tembo a :Elephant .
Kesi is a lion, and lions have conservation status "Endangered" -- but :hasConservationStatus
is never asserted on :Kesi, and it should not be: that property belongs to the species, not to
any one animal in it. Nor is :Kesi himself an instance of :AnimalSpecies --
rdf:type does not chain across levels. :Kesi rdf:type :Lion and
:Lion rdf:type :AnimalSpecies are two separate facts about two different kinds of thing (an
animal, and a species), and neither a DL reasoner nor gUFO's own axioms will combine them into
:Kesi rdf:type :AnimalSpecies. If an ontology needs "what is Kesi's species' conservation
status?" as a queryable fact starting from Kesi, that has to be asked in two steps -- follow
:Kesi's species membership, then check that species' status -- not assumed away as one.
See ontology.ttl for the complete file.
Suppose a modeler adds a fourth species by copying the :Lion pattern, but -- in a hurry --
forgets to extend the disjointness declaration:
:Tiger a owl:Class , :AnimalSpecies ;
rdfs:subClassOf :Animal .
# ...:Lion, :Hyena, :Elephant's owl:AllDisjointClasses list is not updated.
:Rajah a :Lion , :Tiger . # nothing prevents this!
OWL never assumes disjointness by default, so an individual asserted to be both a lion and a tiger raises
no error from a DL reasoner -- there is simply no axiom being violated. But it silently
defeats the entire point of modeling species as a partition. The full mistake is kept in
invalid-example.ttl; the gUFO SHACL shapes catch it with:
gufo:partitions some base type, every pair of its instances must be declared disjoint (via
owl:disjointWith, owl:AllDisjointClasses, or owl:disjointUnionOf); the
shape reports exactly the offending pair, :Lion and :Tiger.gufo.ttl + ontology.ttl is
consistent; run against invalid-example.ttl, this check still passes, confirming OWL
alone does not notice the missing disjointness.ontology.ttl conforms; invalid-example.ttl does not.cd examples/tests
.venv/bin/pytest -v -k 07-higher-order-types