Support owl:AllDisjointClasses in the RDF reader and writer - #289
Open
OnodOfTheNorth wants to merge 1 commit into
Open
Support owl:AllDisjointClasses in the RDF reader and writer#289OnodOfTheNorth wants to merge 1 commit into
OnodOfTheNorth wants to merge 1 commit into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Body
Summary
owl:AllDisjointClassesis missing fromvocab.rs, so n-ary class disjointness is unsupported in the RDF codec in both directions. The read side silently drops the axioms. The write side silently weakens them.Both are reproducible against the Pizza ontology, which uses the construct nine times.
The read side: axioms silently dropped
owl:AllDisjointClassesnever interns as an OWL vocabulary term, so it arrives at the blank-node dispatch inio/rdf/reader.rsas a plainTerm::Iri, matches nothing, and lands in the returnedIncompleteParse. The parse reports success.Reproduction, complete file:
Expected: one
DisjointClasses(A B C).Actual before this change: three
DeclareClassand nothing else.incomplete.bnodeandincomplete.bnode_seqeach hold one entry.Against real data,
https://protege.stanford.edu/ontologies/pizza/pizza.owlcontains nineowl:AllDisjointClassesaxioms (member counts 3, 3, 4, 4, 4, 5, 8, 13, 23). Protégé 5.6.7 reports 14DisjointClassesfor that file. horned-owl reports 5, the pairwiseowl:disjointWithones.The write side: axioms silently weakened
This is the more serious half.
DisjointClassesrenders vianary(), which emits a star from the first member:naryemitsfirst pred each_other, soDisjointClasses(A B C)becomes:B disjointWith Cis never written. Disjointness is not transitive, so the serialized output asserts strictly less than the input axiom. Round-trippingDisjointClasses(A B C)through RDF/XML or Turtle yields two axioms,DisjointClasses(A B)andDisjointClasses(A C), and the third constraint is gone.The read-side gap masked this: the reader could never construct an n-ary
DisjointClasses, so the writer was never handed one in a round-trip test.Note that
nary's star is fine for the transitive n-ary axioms it also serves (EquivalentClasses,EquivalentObjectProperties,EquivalentDataProperties,SameIndividual), since transitivity recovers the missing pairs. This PR does not touch those.The fix
Per the OWL 2 Mapping to RDF Graphs,
DisjointClassesmaps toowl:disjointWithfor two operands and to a blank node typedowl:AllDisjointClasseswithowl:membersfor three or more. That is exactly what the existingmembers()helper inio/rdf/writer.rsalready does forDifferentIndividuals, and the comment above that call already anticipates wider use:Three changes:
src/vocab.rsadd(OWL, AllDisjointClasses, false)to theOWLvocabulary table, plus its assertion intest_meta_owl.src/io/rdf/reader.rsread the construct, reusing the existingretrieve_to_ce_seq.src/io/rdf/writer.rsrouteDisjointClassesthroughmembers()instead ofnary():One thing worth a reviewer's attention
The obvious place for the reader change is a new arm in the match in
axioms(), mirroring the two existingowl:AllDifferentarms. That does not build on Windows.Adding one arm there tips rustc over its default stack: exit code
0xc0000005(STATUS_ACCESS_VIOLATION) with no diagnostic emitted at all. Cleandevelbuilds in about 14s; the same tree with one extra arm in that match does not build unlessRUST_MIN_STACKis raised, at which point it builds in about the same time. So it is a compile-time stack limit rather than anything about the added logic.This PR therefore consumes the matching blank nodes in a small standalone pass,
all_disjoint_classes(), which runs immediately beforeaxioms()and removes what it handles. The match inaxioms()is left byte-identical to what it is today, so it costs that match nothing and the build stays clean at the default stack: 11.5s on this machine, no environment variables.Flagging it because the placement looks arbitrary and is not, and because it means that match is currently one arm away from being unbuildable on Windows for whoever adds the next construct. That is worth knowing independently of this PR.
Verification
Against horned-owl's own test data.
src/ont/owl-rdf/manual/family.owlcontains anowl:AllDisjointClassesaxiom (line 768), andsrc/ont/owl-ttl/manual/family.ttlcontains the same (line 485). The construct appears nowhere insrc/outside those fixtures. Loadingfamily.owlthrough the RDF reader:DisjointClassesarities[2][2, 3]The three-member axiom is the
owl:AllDisjointClassesone. The unconsumed group going to zero is the same fact from the other side: that blank node was the axiom, it was being reported inIncompleteParse, andfamily.owlnow parses completely where it did not before.Against the Pizza ontology (
https://protege.stanford.edu/ontologies/pizza/pizza.owl, which contains nine of these): the reader now yields 14DisjointClasseswith member counts[2,2,2,2,2,3,3,4,4,4,5,8,13,23], matching what Protégé 5.6.7 reports for the same file. Every other metric Protégé reports for it also matches: 100 classes, 8 object properties, 0 data properties, 5 individuals, 12 annotation properties, 120 declaration axioms, 259SubClassOf, 15EquivalentClasses.Round-trip.
DisjointClasses(A B C)now survives as a single three-member axiom through functional syntax, OWL/XML, RDF/XML and Turtle. The two-member form still serialises as a plainowl:disjointWithwith no blank node.No regression in the shared code path.
DifferentIndividualsuses the samemembers()writer helper and the same blank-node shape, and still round-trips at member counts 2, 3, 5 and 41.test_meta_owlcovers the new vocabulary entry.Note:
cargo testdoes not currently compile on Windows on cleandevel, for a reason unrelated to this change.src/io/mod.rs:328importsstd::os::unix::fs::PermissionsExtunconditionally and line 437 callsPermissions::from_mode. A two-linecfggate fixes it; happy to send that separately as it is independent of everything here. The verification above was therefore done through an example binary rather than the test harness.Related, not in this PR
Three further RDF-codec issues found while investigating. Happy to file separately or fold in, whichever suits.
owl:AllDisjointPropertiesis in the vocabulary but the reader has no case for it. The writer emits it, soDisjointObjectProperties(A B C)andDisjointDataProperties(A B C)are written correctly and then read back as zero axioms. The fix wants an n-ary version ofdistinguish_retrieve_property_term_pair_kind;retrieve_to_seq'sfn-pointer parameter cannot carry theicargument the single-termdistinguish_retrieve_property_kindneeds, so it needs a hand-rolled loop plus a homogeneity check.owl:intersectionOf/owl:unionOf/owl:oneOfon a named class subject is not read. Per the mapping,X owl:intersectionOf SEQwith namedXmeansEquivalentClasses(X, ObjectIntersectionOf(...)). Only the anonymous-subject form is handled. Reproduction:loads as declarations only. The W3C wine ontology is built almost entirely on this idiom and loses 62 statements, 63 class expressions and 61 sequences.
xml:base, and when a relativeIRI="..."reference fails CURIE expansion it is passed through as though already absolute, producing a malformed IRI rather than an error. Protégé's OWL/XML writer emits base-relative references by default (IRI="/pizza.owl#American"againstxml:base="http://www.co-ode.org/ontologies/pizza"), so this affects any Protégé-authored.owx. Separate draft inscratch/.Filing notes for him
xml:basedraft. Item 3 is that draft.core/tests/roundtrip.rs::pizza_matches_protege_oracleandcore/tests/known_lossy_expectations.rs. They would need reshaping to horned-owl's own test conventions before offering them.