unicode-logic-kit 0.31.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. unicode_logic_kit/__init__.py +385 -0
  2. unicode_logic_kit/__main__.py +520 -0
  3. unicode_logic_kit/_deadline.py +219 -0
  4. unicode_logic_kit/ace/__init__.py +126 -0
  5. unicode_logic_kit/ace/_align.py +135 -0
  6. unicode_logic_kit/ace/chem_lexicon.py +128 -0
  7. unicode_logic_kit/ace/drs_reader.py +570 -0
  8. unicode_logic_kit/ace/mapping.py +666 -0
  9. unicode_logic_kit/ace/reverse_modal.py +138 -0
  10. unicode_logic_kit/ace/runner.py +551 -0
  11. unicode_logic_kit/ace/translate.py +452 -0
  12. unicode_logic_kit/ace/verbalize.py +1070 -0
  13. unicode_logic_kit/api.py +1284 -0
  14. unicode_logic_kit/atp/__init__.py +177 -0
  15. unicode_logic_kit/atp/_ascii_names.py +113 -0
  16. unicode_logic_kit/atp/_html.py +72 -0
  17. unicode_logic_kit/atp/_substructural_input.py +228 -0
  18. unicode_logic_kit/atp/_tff_problem.py +715 -0
  19. unicode_logic_kit/atp/_tptp_problem.py +1111 -0
  20. unicode_logic_kit/atp/_writer_support.py +289 -0
  21. unicode_logic_kit/atp/clingo_backend.py +1180 -0
  22. unicode_logic_kit/atp/cvc5_backend.py +1385 -0
  23. unicode_logic_kit/atp/eprover_backend.py +732 -0
  24. unicode_logic_kit/atp/finite_domain.py +1055 -0
  25. unicode_logic_kit/atp/fitch.py +1547 -0
  26. unicode_logic_kit/atp/fitch_search.py +551 -0
  27. unicode_logic_kit/atp/hets_backend.py +339 -0
  28. unicode_logic_kit/atp/hybrid_down.py +120 -0
  29. unicode_logic_kit/atp/incremental.py +250 -0
  30. unicode_logic_kit/atp/kripke_enum.py +741 -0
  31. unicode_logic_kit/atp/lambek.py +436 -0
  32. unicode_logic_kit/atp/leo3_backend.py +332 -0
  33. unicode_logic_kit/atp/linear.py +738 -0
  34. unicode_logic_kit/atp/lj.py +705 -0
  35. unicode_logic_kit/atp/logic_backends.py +566 -0
  36. unicode_logic_kit/atp/ltl_tableau.py +1084 -0
  37. unicode_logic_kit/atp/minizinc_backend.py +1402 -0
  38. unicode_logic_kit/atp/modal_tableau.py +1382 -0
  39. unicode_logic_kit/atp/nanocop_backend.py +410 -0
  40. unicode_logic_kit/atp/portfolio.py +489 -0
  41. unicode_logic_kit/atp/protocol.py +1803 -0
  42. unicode_logic_kit/atp/prover9_entailment.py +1153 -0
  43. unicode_logic_kit/atp/resolution.py +1376 -0
  44. unicode_logic_kit/atp/resolution_check.py +1114 -0
  45. unicode_logic_kit/atp/sequent.py +1050 -0
  46. unicode_logic_kit/atp/tableau.py +921 -0
  47. unicode_logic_kit/atp/tableau_check.py +543 -0
  48. unicode_logic_kit/atp/tptp_ncl.py +811 -0
  49. unicode_logic_kit/atp/tptp_tff.py +1546 -0
  50. unicode_logic_kit/atp/tstp.py +1333 -0
  51. unicode_logic_kit/atp/tstp_check.py +1096 -0
  52. unicode_logic_kit/atp/twee_backend.py +236 -0
  53. unicode_logic_kit/atp/twee_check.py +711 -0
  54. unicode_logic_kit/atp/twee_entailment.py +953 -0
  55. unicode_logic_kit/atp/vampire_entailment.py +540 -0
  56. unicode_logic_kit/atp/z3_arith.py +470 -0
  57. unicode_logic_kit/atp/z3_equivalence.py +36 -0
  58. unicode_logic_kit/atp/z3_fuzzy.py +362 -0
  59. unicode_logic_kit/atp/z3_input.py +500 -0
  60. unicode_logic_kit/atp/z3_models.py +208 -0
  61. unicode_logic_kit/chem/__init__.py +88 -0
  62. unicode_logic_kit/chem/_naming.py +284 -0
  63. unicode_logic_kit/chem/cache.py +185 -0
  64. unicode_logic_kit/chem/interop.py +244 -0
  65. unicode_logic_kit/chem/mol.py +525 -0
  66. unicode_logic_kit/chem/signature.py +112 -0
  67. unicode_logic_kit/comorphism.py +497 -0
  68. unicode_logic_kit/dl/__init__.py +384 -0
  69. unicode_logic_kit/dl/classification.py +227 -0
  70. unicode_logic_kit/dl/concepts.py +632 -0
  71. unicode_logic_kit/dl/datatypes.py +818 -0
  72. unicode_logic_kit/dl/owl_functional.py +2433 -0
  73. unicode_logic_kit/dl/owl_manchester.py +1637 -0
  74. unicode_logic_kit/dl/owl_reasoner.py +790 -0
  75. unicode_logic_kit/dl/parser.py +391 -0
  76. unicode_logic_kit/dl/tableau.py +4048 -0
  77. unicode_logic_kit/dl/translate.py +2704 -0
  78. unicode_logic_kit/drt/__init__.py +94 -0
  79. unicode_logic_kit/drt/export.py +179 -0
  80. unicode_logic_kit/drt/nodes.py +506 -0
  81. unicode_logic_kit/drt/parser.py +965 -0
  82. unicode_logic_kit/drt/resolve.py +195 -0
  83. unicode_logic_kit/drt/reverse.py +175 -0
  84. unicode_logic_kit/eval/__init__.py +106 -0
  85. unicode_logic_kit/eval/batch.py +382 -0
  86. unicode_logic_kit/eval/canonical.py +663 -0
  87. unicode_logic_kit/eval/chem_batch.py +606 -0
  88. unicode_logic_kit/eval/converses.py +200 -0
  89. unicode_logic_kit/eval/datasets/__init__.py +136 -0
  90. unicode_logic_kit/eval/datasets/_base.py +263 -0
  91. unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
  92. unicode_logic_kit/eval/datasets/c3po.py +678 -0
  93. unicode_logic_kit/eval/datasets/folio.py +158 -0
  94. unicode_logic_kit/eval/datasets/fracas.py +418 -0
  95. unicode_logic_kit/eval/datasets/groves.py +191 -0
  96. unicode_logic_kit/eval/datasets/logicbench.py +467 -0
  97. unicode_logic_kit/eval/datasets/logicnli.py +303 -0
  98. unicode_logic_kit/eval/datasets/malls.py +133 -0
  99. unicode_logic_kit/eval/datasets/pfolio.py +594 -0
  100. unicode_logic_kit/eval/datasets/pmb.py +242 -0
  101. unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
  102. unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
  103. unicode_logic_kit/eval/datasets/proverqa.py +674 -0
  104. unicode_logic_kit/eval/datasets/willow.py +478 -0
  105. unicode_logic_kit/eval/equivalence.py +466 -0
  106. unicode_logic_kit/eval/exercise_gen.py +533 -0
  107. unicode_logic_kit/eval/explain.py +791 -0
  108. unicode_logic_kit/eval/generality.py +750 -0
  109. unicode_logic_kit/eval/metric_hf.py +458 -0
  110. unicode_logic_kit/eval/predicate_match.py +343 -0
  111. unicode_logic_kit/eval/theory_check.py +1170 -0
  112. unicode_logic_kit/eval/validate.py +306 -0
  113. unicode_logic_kit/fol/__init__.py +177 -0
  114. unicode_logic_kit/fol/_atom_keys.py +510 -0
  115. unicode_logic_kit/fol/_fol_nodes.py +3586 -0
  116. unicode_logic_kit/fol/_free_parameters.py +105 -0
  117. unicode_logic_kit/fol/_ho_nodes.py +448 -0
  118. unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
  119. unicode_logic_kit/fol/_identifiers.py +1091 -0
  120. unicode_logic_kit/fol/_lambek_nodes.py +112 -0
  121. unicode_logic_kit/fol/_linear_nodes.py +352 -0
  122. unicode_logic_kit/fol/_modal_nodes.py +1467 -0
  123. unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
  124. unicode_logic_kit/fol/_numeral_symbols.py +231 -0
  125. unicode_logic_kit/fol/_so_nodes.py +200 -0
  126. unicode_logic_kit/fol/_symbol_names.py +81 -0
  127. unicode_logic_kit/fol/_team_nodes.py +181 -0
  128. unicode_logic_kit/fol/_tptp_symbols.py +551 -0
  129. unicode_logic_kit/fol/_truth_constants.py +117 -0
  130. unicode_logic_kit/fol/casl_export.py +1135 -0
  131. unicode_logic_kit/fol/casl_import.py +929 -0
  132. unicode_logic_kit/fol/derivation.py +367 -0
  133. unicode_logic_kit/fol/dialect_detect.py +70 -0
  134. unicode_logic_kit/fol/dialect_repair.py +537 -0
  135. unicode_logic_kit/fol/frames.py +637 -0
  136. unicode_logic_kit/fol/grammars/terminals.lark +31 -0
  137. unicode_logic_kit/fol/lambda_tools.py +297 -0
  138. unicode_logic_kit/fol/latex_input.py +429 -0
  139. unicode_logic_kit/fol/modal_translation.py +944 -0
  140. unicode_logic_kit/fol/msflparser.py +1033 -0
  141. unicode_logic_kit/fol/naming.py +422 -0
  142. unicode_logic_kit/fol/nodes.py +241 -0
  143. unicode_logic_kit/fol/normalforms.py +492 -0
  144. unicode_logic_kit/fol/pal.py +287 -0
  145. unicode_logic_kit/fol/prolog_export.py +566 -0
  146. unicode_logic_kit/fol/prolog_input.py +505 -0
  147. unicode_logic_kit/fol/prover9_input.py +1325 -0
  148. unicode_logic_kit/fol/qml.py +1760 -0
  149. unicode_logic_kit/fol/qmltp_input.py +525 -0
  150. unicode_logic_kit/fol/sanitize.py +221 -0
  151. unicode_logic_kit/fol/serialize.py +79 -0
  152. unicode_logic_kit/fol/signature.py +1290 -0
  153. unicode_logic_kit/fol/simplify_check.py +544 -0
  154. unicode_logic_kit/fol/spans.py +594 -0
  155. unicode_logic_kit/fol/tptp_input.py +1503 -0
  156. unicode_logic_kit/fol/tptp_repair.py +941 -0
  157. unicode_logic_kit/fol/unification.py +157 -0
  158. unicode_logic_kit/fol/verbalize.py +263 -0
  159. unicode_logic_kit/hets/__init__.py +163 -0
  160. unicode_logic_kit/hets/bridge.py +142 -0
  161. unicode_logic_kit/hets/client.py +748 -0
  162. unicode_logic_kit/hets/docker.py +420 -0
  163. unicode_logic_kit/hets/dol.py +712 -0
  164. unicode_logic_kit/hets/haskell_json.py +355 -0
  165. unicode_logic_kit/hets/owl_backend.py +794 -0
  166. unicode_logic_kit/hets/owl_cli.py +598 -0
  167. unicode_logic_kit/hets/symbols.py +512 -0
  168. unicode_logic_kit/hol/__init__.py +140 -0
  169. unicode_logic_kit/hol/_ho_common.py +323 -0
  170. unicode_logic_kit/hol/_isabelle_binders.py +125 -0
  171. unicode_logic_kit/hol/classical.py +812 -0
  172. unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
  173. unicode_logic_kit/hol/deepshallow/_common.py +177 -0
  174. unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
  175. unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
  176. unicode_logic_kit/hol/deepshallow/modal.py +217 -0
  177. unicode_logic_kit/hol/deepshallow/qml.py +406 -0
  178. unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
  179. unicode_logic_kit/hol/free.py +753 -0
  180. unicode_logic_kit/hol/goedel.py +336 -0
  181. unicode_logic_kit/hol/ho_modal.py +1743 -0
  182. unicode_logic_kit/hol/intuitionistic.py +403 -0
  183. unicode_logic_kit/hol/isabelle_conditional.py +593 -0
  184. unicode_logic_kit/hol/isabelle_modal.py +1908 -0
  185. unicode_logic_kit/hol/isabelle_relevant.py +412 -0
  186. unicode_logic_kit/hol/isabelle_runner.py +1147 -0
  187. unicode_logic_kit/hol/isabelle_substructural.py +884 -0
  188. unicode_logic_kit/hol/lean.py +1018 -0
  189. unicode_logic_kit/hol/manyvalued.py +921 -0
  190. unicode_logic_kit/hol/secondorder.py +687 -0
  191. unicode_logic_kit/hol/thf_modal.py +941 -0
  192. unicode_logic_kit/hol/thirdorder.py +397 -0
  193. unicode_logic_kit/ilp/__init__.py +89 -0
  194. unicode_logic_kit/ilp/readback.py +389 -0
  195. unicode_logic_kit/ilp/separation.py +153 -0
  196. unicode_logic_kit/ilp/task.py +730 -0
  197. unicode_logic_kit/logic.py +163 -0
  198. unicode_logic_kit/mcp/__init__.py +28 -0
  199. unicode_logic_kit/mcp/__main__.py +5 -0
  200. unicode_logic_kit/mcp/chem_tools.py +1031 -0
  201. unicode_logic_kit/mcp/server.py +2453 -0
  202. unicode_logic_kit/mcp/syntax_spec.py +681 -0
  203. unicode_logic_kit/prob/__init__.py +53 -0
  204. unicode_logic_kit/prob/_bdd.py +225 -0
  205. unicode_logic_kit/prob/_column_gen.py +668 -0
  206. unicode_logic_kit/prob/distribution.py +686 -0
  207. unicode_logic_kit/prob/nilsson.py +470 -0
  208. unicode_logic_kit/py.typed +0 -0
  209. unicode_logic_kit/semantics/__init__.py +137 -0
  210. unicode_logic_kit/semantics/_modal_reject.py +156 -0
  211. unicode_logic_kit/semantics/action_models.py +466 -0
  212. unicode_logic_kit/semantics/asp_models.py +1200 -0
  213. unicode_logic_kit/semantics/conditional.py +580 -0
  214. unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
  215. unicode_logic_kit/semantics/free_logic.py +913 -0
  216. unicode_logic_kit/semantics/fuzzy.py +384 -0
  217. unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
  218. unicode_logic_kit/semantics/intuitionistic.py +581 -0
  219. unicode_logic_kit/semantics/kripke.py +1139 -0
  220. unicode_logic_kit/semantics/manyvalued.py +580 -0
  221. unicode_logic_kit/semantics/matrix.py +342 -0
  222. unicode_logic_kit/semantics/model_eval.py +1135 -0
  223. unicode_logic_kit/semantics/modelfinder.py +1036 -0
  224. unicode_logic_kit/semantics/nonmonotonic.py +372 -0
  225. unicode_logic_kit/semantics/relevant.py +331 -0
  226. unicode_logic_kit/semantics/secondorder.py +657 -0
  227. unicode_logic_kit/semantics/structures.py +352 -0
  228. unicode_logic_kit/semantics/tarski.py +975 -0
  229. unicode_logic_kit/semantics/team.py +315 -0
  230. unicode_logic_kit/semantics/team_translation.py +416 -0
  231. unicode_logic_kit/semantics/thirdorder.py +358 -0
  232. unicode_logic_kit/semantics/tnorm.py +85 -0
  233. unicode_logic_kit/semantics/truthtable.py +201 -0
  234. unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
  235. unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
  236. unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
  237. unicode_logic_kit-0.31.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,384 @@
1
+ """The ``dl`` subpackage — the description logic **ALC**.
2
+
3
+ Concept constructors (``import unicode_logic_kit.dl as dl``)::
4
+
5
+ dl.Top(), dl.Bottom(), dl.Atomic("Person"), dl.Not(C),
6
+ dl.And(C, D), dl.Or(C, D), dl.Exists("hasChild", C), dl.ForAll("hasChild", C),
7
+ dl.AtLeast(2, "hasChild", C), dl.AtMost(1, "hasChild", C) # qualified number
8
+ # restrictions (ALCQ)
9
+
10
+ Reasoning over a (general) TBox / ABox::
11
+
12
+ dl.concept_satisfiable(C, tbox) # is C satisfiable w.r.t. the TBox?
13
+ dl.subsumes(C, D, tbox) # does the TBox entail C ⊑ D?
14
+ dl.equivalent(C, D, tbox) # C ≡ D?
15
+ dl.abox_consistent(abox, tbox) # is the knowledge base consistent?
16
+
17
+ Instance/realization queries over an ABox, all pure reductions to ``abox_consistent``
18
+ (see :mod:`unicode_logic_kit.dl.tableau`)::
19
+
20
+ dl.instance_check(abox, "alice", C, tbox) # does the KB entail alice : C?
21
+ dl.instance_retrieval(abox, C, tbox) # every individual entailed to be C
22
+ dl.realize(abox, "alice", vocabulary, tbox) # alice's most-specific concepts
23
+ dl.realize_all(abox, vocabulary, tbox) # realize() for every individual
24
+
25
+ TBox classification — the whole named-concept subsumption hierarchy as a pure
26
+ reduction to ``subsumes`` (see :mod:`unicode_logic_kit.dl.classification`)::
27
+
28
+ dl.classify(tbox) # -> Classification(equivalents, parents,
29
+ # children, ancestors)
30
+
31
+ ALC is exactly multi-modal K; the reasoner is a tableau with TBox internalisation
32
+ and subset blocking (see :mod:`unicode_logic_kit.dl.tableau`).
33
+
34
+ Parsing the glyph concept syntax back into a :class:`Concept`
35
+ (:mod:`unicode_logic_kit.dl.parser`)::
36
+
37
+ dl.parse_concept("∃hasChild.(Doctor ⊓ ¬Rich)") # -> Concept
38
+ dl.parse_gci("Doctor ⊑ ∃hasChild.⊤") # -> (Concept, Concept)
39
+
40
+ Parsing/rendering the W3C OWL 2 Manchester Syntax (ALC fragment only,
41
+ :mod:`unicode_logic_kit.dl.owl_manchester`)::
42
+
43
+ dl.parse_manchester("hasChild some (Doctor and not Rich)") # -> Concept
44
+ dl.to_manchester(C) # -> str
45
+ dl.parse_manchester_axiom("Doctor SubClassOf hasChild some owl:Thing")
46
+ # -> ("subclass", Concept, Concept)
47
+ dl.parse_manchester_role_axiom("hasChild SubPropertyOf hasDescendant")
48
+ # -> ("subproperty", "hasChild", "hasDescendant")
49
+ dl.parse_manchester_role_axiom("hasDescendant Characteristics: Transitive")
50
+ # -> ("transitive", "hasDescendant")
51
+ dl.parse_manchester_role_axiom("partOf InverseOf hasPart")
52
+ # -> ("inverse", "partOf", "hasPart")
53
+ dl.parse_manchester_role_axiom("hasSink DisjointWith hasSource")
54
+ # -> ("disjoint", "hasSink", "hasSource")
55
+
56
+ Parsing/rendering the W3C OWL 2 Functional-Style Syntax (ALCHQ fragment, a whole
57
+ ontology document rather than a single axiom, :mod:`unicode_logic_kit.dl.owl_functional`)::
58
+
59
+ dl.parse_owl_functional("Ontology(SubClassOf(Doctor Person))") # -> (TBox, ABox)
60
+ dl.to_owl_functional(tbox, abox) # -> str
61
+ dl.parse_owl_functional_class_expression("ObjectSomeValuesFrom(hasChild Doctor)")
62
+ # -> Exists("hasChild", Atomic("Doctor"))
63
+ dl.to_owl_functional_class_expression(C) # -> str
64
+
65
+ The standard translation to FOL, so ALC reuses the kit's FOL provers/exports
66
+ (:mod:`unicode_logic_kit.dl.translate`)::
67
+
68
+ dl.concept_to_fol(C) # -> FOL formula π(C, x), one free var x
69
+ dl.subsumption_to_fol(C, D) # -> ∀x (π(C, x) → π(D, x))
70
+ dl.tbox_to_fol(tbox) # -> conjunction of subsumption_to_fol per GCI
71
+ dl.rbox_to_fol(tbox) # -> conjunction of role-inclusion/transitivity closures
72
+ dl.abox_to_fol(abox) # -> conjunction of the ABox's assertions
73
+ dl.kb_to_fol(tbox, abox) # -> KnowledgeBaseFOL: the KB + the role box as SEPARATE axioms
74
+ dl.check_kb_names(tbox, abox) # -> None: kb_to_fol's check on names, over boxes rendered apart
75
+ dl.concept_to_modal(C) # -> propositional modal-K formula (single role only)
76
+
77
+ A :class:`TBox` holds the concept inclusions AND the role box, but ``tbox_to_fol``
78
+ renders the concept inclusions only — so it RAISES ``dl.RoleBoxOmittedError`` for a TBox
79
+ that has role inclusions or transitive roles (``concept_inclusions_only=True`` is the
80
+ explicit opt-out). Ask any question *relative to* a knowledge base through
81
+ ``kb_to_fol``, passing the role box as premises, never folded into the formula::
82
+
83
+ kb = dl.kb_to_fol(tbox, abox)
84
+ api.prove(kb.subsumption_goal(C, D), kb.tbox_premises) # = dl.subsumes(C, D, tbox)
85
+ api.prove(kb.unsatisfiability_goal(C), kb.tbox_premises) # proved <=> C is unsatisfiable
86
+ api.prove(kb.instance_goal("a", C), kb.premises) # = dl.instance_check(...)
87
+ api.prove(fol.Not(kb.formula), kb.axioms) # proved <=> the knowledge base is inconsistent
88
+
89
+ Role hierarchies and transitive roles (RBox), on top of the concept-level TBox
90
+ (:mod:`unicode_logic_kit.dl.tableau`)::
91
+
92
+ t = dl.TBox().add_role_inclusion("hasChild", "hasDescendant").add_transitive_role("hasDescendant")
93
+ dl.subsumes(dl.Exists("hasChild", dl.Exists("hasChild", C)), dl.Exists("hasDescendant", C), t)
94
+ # -> True: ∃hasChild.∃hasChild.C ⊑ ∃hasDescendant.C, entailed by the RBox alone
95
+
96
+ Qualified number restrictions (ALCQ), on SIMPLE roles only (a role that is
97
+ transitive, or has a transitive sub-role, raises ``NonSimpleRoleError`` by name —
98
+ see :mod:`unicode_logic_kit.dl.tableau`'s "Qualified number restrictions" section)::
99
+
100
+ dl.concept_satisfiable(dl.And(dl.AtLeast(2, "r", C), dl.AtMost(1, "r", dl.Top())))
101
+ # -> False: at least 2 pairwise-distinct r-successors but at most 1 r-successor at all
102
+
103
+ ``ABox.assert_distinct(a, b)`` records ``a ≠ b`` — this reasoner has no unique name
104
+ assumption (see the same tableau section), so two ABox individuals otherwise remain
105
+ mergeable, which matters as soon as a number restriction is in play.
106
+
107
+ ``instance_check``/``classify``/the rest of the reasoning API above respect a TBox's
108
+ RBox automatically, since they all reduce to ``concept_satisfiable``/``abox_consistent``
109
+ — no separate RBox-aware entry point is needed anywhere else.
110
+
111
+ The rest of the OWL 2 object property box
112
+ ------------------------------------------
113
+ A :class:`TBox` carries every OWL 2 object-property axiom kind, and
114
+ ``dl.rbox_to_fol`` renders every one of them (so ``kb_to_fol`` + ``api.prove``
115
+ and ``dl.external_*`` answer them all). What the IN-HOUSE tableau does with
116
+ each is the second column of ``dl.tableau._AXIOM_KINDS``::
117
+
118
+ t = (dl.TBox()
119
+ .add_disjoint_roles("hasSink", "hasSource") # decided: a clash rule
120
+ .add_asymmetric_role("hasPhysicalInput") # decided: a clash rule
121
+ .add_irreflexive_role("hasPhysicalInput") # decided: a clash rule
122
+ .add_functional_role("hasState")) # decided: ⊤ ⊑ ≤1 P.⊤
123
+ dl.abox_consistent(dl.ABox().assert_role("a", "a", "hasPhysicalInput"), t)
124
+ # -> False: Irr(hasPhysicalInput) is ∀x ¬hasPhysicalInput(x, x)
125
+
126
+ dl.concept_satisfiable(dl.Top(), dl.TBox().add_symmetric_role("r"))
127
+ # -> raises dl.UnsupportedAxiomError, naming SymmetricObjectProperty and
128
+ # saying why (a symmetric role IS the inverse-role inclusion r ⊑ r⁻,
129
+ # and a back-edge breaks subset blocking) and what to use instead
130
+
131
+ Refused by name, each with its own remedy in the message:
132
+ ``InverseObjectProperties`` (:meth:`~unicode_logic_kit.dl.TBox.add_inverse_roles`),
133
+ ``SymmetricObjectProperty``, ``ReflexiveObjectProperty``,
134
+ ``InverseFunctionalObjectProperty`` and a property chain
135
+ (:meth:`~unicode_logic_kit.dl.TBox.add_role_chain`) — plus an
136
+ :class:`~unicode_logic_kit.dl.concepts.InverseRole` on either side of a role
137
+ inclusion, which is accepted and rendered correctly but not decided in house.
138
+ ``EquivalentObjectProperties``
139
+ (:meth:`~unicode_logic_kit.dl.TBox.add_equivalent_roles`) needs no rule at all:
140
+ it is stored as the role inclusions it abbreviates.
141
+
142
+ OWL 2 (Structural Specification §11) restricts asymmetry, irreflexivity, role
143
+ disjointness, functionality and inverse-functionality — and every qualified
144
+ number restriction — to SIMPLE roles, and the kit enforces exactly that
145
+ condition, by name (:class:`NonSimpleRoleError`), on the in-house AND the
146
+ external route. A role-box builder handed something that is not a usable role
147
+ in that position raises :class:`RoleExpressionError` on the call that is wrong::
148
+
149
+ dl.TBox().add_role_inclusion(("r", "s"), "t")
150
+ # -> raises dl.RoleExpressionError: a sequence of roles is a PROPERTY
151
+ # CHAIN -- use dl.TBox.add_role_chain(chain, super_role)
152
+ dl.TBox().add_role_inclusion("P", "owl:topObjectProperty")
153
+ # -> raises dl.RoleExpressionError: an OWL 2 BUILT-IN property name, not
154
+ # an ordinary role. ``dl.parse_owl_functional`` consumes the
155
+ # TAUTOLOGICAL inclusion into it as a documented no-op; every other use
156
+ # of a built-in name is refused by name there too.
157
+
158
+ Class expressions and the ABox: value restrictions, domain/range, identity
159
+ ---------------------------------------------------------------------------
160
+ ``dl.HasValue(role, individual)`` is OWL's ``ObjectHasValue(r a)``, written
161
+ ``exists r.{a}``: a NOMINAL in disguise, so the in-house tableau REFUSES it, as
162
+ it refuses a bare ``Nominal`` (see :class:`~unicode_logic_kit.dl.concepts.HasValue`
163
+ for why it is a constructor of its own and not ``dl.Exists(role,
164
+ dl.Nominal(a))``, and "Value restrictions (ObjectHasValue)" in
165
+ :mod:`unicode_logic_kit.dl.tableau` for why no in-house rule for it is sound: a
166
+ value restriction gives a generated node an edge back to a named one, which the
167
+ subset blocking the tableau terminates by does not cover). Its FOL image is the
168
+ GROUND ATOM, the one-point reduction of ``exists y (r(x, y) and y = a)``, and
169
+ that image, with ``api.prove`` — or ``dl.external_*`` (HermiT) — is what decides
170
+ it::
171
+
172
+ dl.concept_to_fol(dl.HasValue("HasStateOfMatter", "Liquid"))
173
+ # -> HasStateOfMatter(x, Liquid)
174
+ dl.concept_satisfiable(dl.And(dl.HasValue("r", "a"), dl.ForAll("r", dl.Bottom())))
175
+ # -> raises dl.UnsupportedConceptError, naming the value restriction and
176
+ # the routes that do decide it. The FOL image proves the hand-derived
177
+ # verdict (unsatisfiable: a would have to be in bottom).
178
+
179
+ ``TBox.add_role_domain`` / ``add_role_range`` store
180
+ ``ObjectPropertyDomain``/``ObjectPropertyRange`` NATIVELY rather than as the
181
+ GCIs they are equivalent to, so the image is the direct two-variable sentence
182
+ and ``to_owl_functional`` round-trips the axiom to itself; the tableau decides
183
+ them by internalising those GCIs, with no new rule::
184
+
185
+ dl.rbox_to_fol(dl.TBox().add_role_domain("Covers", dl.Atomic("Study")))
186
+ # -> forall x forall y (Covers(x, y) -> Study(x))
187
+
188
+ ``ABox.assert_same`` (``SameIndividual``) is decided by node MERGING, closed
189
+ under the equivalence the assertions generate, before any completion rule runs;
190
+ ``ABox.assert_negative_role`` (``NegativeObjectPropertyAssertion``, arguments in
191
+ ``assert_role``'s order) by a clash condition over FORBIDDEN edges. The second
192
+ refuses a NON-SIMPLE role by name, since seeing a forbidden edge is what its
193
+ verdict depends on and the tableau never materialises a transitive role's
194
+ derived edges::
195
+
196
+ dl.abox_consistent(dl.ABox().assert_same("a", "b").assert_distinct("a", "b"))
197
+ # -> False: a = b and a != b have no common model
198
+
199
+ ``dl.parse_owl_functional_axioms(text)`` is the per-axiom reader for a whole
200
+ ontology: it returns an :class:`OwlFunctionalResult` with the TBox/ABox it could
201
+ build plus one :class:`RefusedAxiom` per axiom outside the fragment, each naming
202
+ the construct, its offset and its own source text. ``dl.parse_owl_functional``
203
+ stays strict, raising on the first such construct. The lenient reader recovers
204
+ from :class:`OwlFunctionalUnsupportedError` and nothing else: MALFORMED input
205
+ still raises::
206
+
207
+ doc = dl.parse_owl_functional_axioms(text)
208
+ doc.ok, doc.accepted, doc.refused_keywords
209
+ kb = doc.to_kb() # == dl.kb_to_fol(doc.tbox, doc.abox)
210
+
211
+ One limit to know before printing an image that mentions an individual: the kit
212
+ decides predicate-versus-term by the first character's case, so an UPPER-case
213
+ individual name -- the norm for an OWL IRI -- prints as itself and does not read
214
+ back as the same formula (``Alice = Bob`` does not parse;
215
+ ``HasStateOfMatter(x, Liquid)`` parses only as third-order, with ``Liquid`` a
216
+ ``PredicateTerm``). The AST is sound either way, and the routes that never go
217
+ through text are unaffected. See ``tests/test_printed_text_reads_back.py``.
218
+
219
+ Inverse roles and nominals (I, O) — outside ALCHQ, refused by the in-house tableau
220
+ (a value restriction, ``dl.HasValue``, is a nominal and is refused with them)::
221
+
222
+ dl.concept_satisfiable(dl.Exists(dl.InverseRole("hasChild"), dl.Top()))
223
+ # -> raises dl.UnsupportedConceptError: no in-house tableau rule decides I/O
224
+
225
+ ``dl.translate`` translates both constructs faithfully to FOL regardless (a role
226
+ expression has a standard FOL image, argument order swapped; a nominal ``{a}``
227
+ becomes the equality ``x = a`` — see :mod:`unicode_logic_kit.dl.translate`'s module
228
+ docstring). To actually DECIDE a concept that needs either, use
229
+ :mod:`unicode_logic_kit.dl.owl_reasoner`'s external, HermiT-backed reasoner (an
230
+ optional dependency — install with ``pip install unicode-logic-kit[owl]``)::
231
+
232
+ dl.owl_reasoner_available() # owlready2 importable?
233
+ dl.external_concept_satisfiable(dl.And(dl.Nominal("a"), dl.Nominal("b")))
234
+ # -> True: {a} ⊓ {b} is satisfiable absent an explicit DifferentIndividuals
235
+ # assertion (OWL 2 has no unique name assumption)
236
+ dl.external_instance_check(
237
+ dl.ABox().assert_role("alice", "bob", "hasChild"), "bob",
238
+ dl.Exists(dl.InverseRole("hasChild"), dl.Top()))
239
+ # -> True: bob has an incoming hasChild edge, i.e. an hasChild-inverse successor
240
+
241
+ Which route decides what — the axiom-kind table
242
+ ------------------------------------------------
243
+ Every axiom kind a :class:`TBox`/:class:`ABox` can hold has ONE row in
244
+ ``dl.tableau._AXIOM_KINDS``, and the row says what the in-house tableau does
245
+ with it and what the FOL image does with it (see "The axiom-kind table" in
246
+ :mod:`unicode_logic_kit.dl.tableau`'s module docstring). Two rules follow, and
247
+ they are the kit's answer to "could these two routes disagree?":
248
+
249
+ * **Builders never refuse.** ``TBox.add_*``/``ABox.assert_*`` accept every
250
+ kind, because a TBox is what a parser fills from a file.
251
+ * **A kind the tableau has no rule for is refused BY NAME at query time**, by
252
+ :class:`UnsupportedAxiomError`, from ``concept_satisfiable``/
253
+ ``abox_consistent`` — and so from everything that reduces to them. Never
254
+ approximated, never silently ignored. ``dl.kb_to_fol`` + ``api.prove``, or
255
+ ``dl.external_*``, is where such a kind is answered instead.
256
+
257
+ The FOL image keeps the two apart as well: ``kb_to_fol(tbox, abox).formula``
258
+ is the knowledge base and ``.side_axioms`` the premises that are NOT part of
259
+ it (each a :class:`SideAxiom` carrying the OWL ``kind`` it came from, so
260
+ ``kb.axioms_of_kind("TransitiveObjectProperty")`` is a question with an
261
+ answer), while ``tbox_to_fol`` refuses outright to hand back a concept-
262
+ inclusion image that silently drops them.
263
+
264
+ The data layer: two sorts
265
+ --------------------------
266
+ OWL 2's second sort -- data values, datatypes, data properties -- is STORED
267
+ (:class:`DataExists`, :class:`DataForAll`, :class:`DataHasValue`,
268
+ :class:`DataAtLeast`, :class:`DataAtMost`; ``TBox.add_data_property_*`` and
269
+ ``add_datatype_definition``; ``ABox.assert_data``/``assert_negative_data``;
270
+ :class:`Literal` and the data ranges :class:`Datatype`,
271
+ :class:`DatatypeRestriction`, :class:`DataOneOf`, :class:`DataComplementOf`,
272
+ :class:`DataIntersectionOf`, :class:`DataUnionOf`), READ and WRITTEN by the
273
+ Manchester and Functional-Style syntaxes, and TRANSLATED by the FOL image --
274
+ and REFUSED by the in-house tableau, by name, kind by kind (it has no data
275
+ domain), and by ``dl.owl_reasoner``. What answers is ``dl.kb_to_fol`` +
276
+ ``api.prove``; facets are decided by ``atp.z3_arith``.
277
+
278
+ The image is a guarded ONE-sorted theory over two reserved predicates,
279
+ :data:`OWL_THING` and :data:`OWL_DATA`. The separation of the two domains, the
280
+ typing of every data property and individual, the datatype lattice and literal
281
+ distinctness are SIDE axioms (``group`` ``"sort"``/``"datatype"``), so the
282
+ two-sorted question is the one the prover answers only if they are PREMISES:
283
+ pass ``kb.premises`` / ``kb.tbox_premises``, not ``kb.formula`` alone, and let the
284
+ bundle build the GOAL, ``kb.subsumption_goal(C, D)``, ``kb.unsatisfiability_goal(C)``
285
+ or ``kb.instance_goal(a, C)``, which relativise it the way ``kb.separation``
286
+ says (a hand-built goal needs ``subsumption_to_fol(..., object_sort=True)``).
287
+ Every GCI of such an image is restricted to ``OwlThing`` (an unrestricted
288
+ ``⊤ ⊑ {a}`` would range over data values and make an OWL-consistent knowledge
289
+ base inconsistent). The sort, typing and datatype axioms are derived from the
290
+ vocabulary the knowledge base uses, so name the concepts you are going to ask
291
+ about when you build it, ``dl.kb_to_fol(tbox, abox, query=[C, D])`` -- a goal over
292
+ a name the bundle does not cover is refused by name, not answered wrongly. The
293
+ image is sound -- every OWL model expands to a model of it, so ``proved``
294
+ transfers to OWL 2 -- and deliberately not complete: facets are uninterpreted
295
+ for ``api.prove``, a literal is typed only by the datatype it was written with,
296
+ and the size of a value space is not stated, so ``refuted`` does not transfer
297
+ (``kb.refutation_is_decisive`` is ``False`` exactly when there is a data layer).
298
+ See "The data layer: two sorts" in the description-logic guide.
299
+
300
+ Every ``dl.external_*`` function mirrors its ``dl.<name>`` in-house-tableau
301
+ namesake's signature and reduction exactly (``external_subsumes`` reduces to
302
+ ``external_concept_satisfiable`` the way ``subsumes`` reduces to
303
+ ``concept_satisfiable``, and so on), just decided over the bigger ALCHQ + I + O
304
+ fragment instead — see :mod:`unicode_logic_kit.dl.owl_reasoner`'s module docstring.
305
+ """
306
+
307
+ from .concepts import (
308
+ Concept, Top, Bottom, Atomic, Not, And, Or, Exists, ForAll, AtLeast, AtMost,
309
+ InverseRole, Nominal, HasValue, DataExists, DataForAll, DataHasValue,
310
+ DataAtLeast, DataAtMost, nnf,
311
+ )
312
+ from .datatypes import (
313
+ Literal, DataRange, Datatype, DatatypeRestriction, DataOneOf,
314
+ DataComplementOf, DataIntersectionOf, DataUnionOf, UnsupportedDatatypeError,
315
+ OWL_THING, OWL_DATA, datarange_to_fol,
316
+ )
317
+ from .tableau import (
318
+ TBox, ABox,
319
+ concept_satisfiable, concept_unsatisfiable, subsumes, equivalent, abox_consistent,
320
+ instance_check, instance_retrieval, realize, realize_all,
321
+ NonSimpleRoleError, UnsupportedConceptError, UnsupportedAxiomError,
322
+ RoleExpressionError,
323
+ )
324
+ from .classification import classify, Classification
325
+ from .parser import parse_concept, parse_gci, ConceptSyntaxError
326
+ from .translate import (
327
+ concept_to_fol, subsumption_to_fol, tbox_to_fol, rbox_to_fol, abox_to_fol,
328
+ databox_to_fol, data_sort_axioms, check_kb_names,
329
+ kb_to_fol, KnowledgeBaseFOL, SideAxiom, RoleBoxOmittedError,
330
+ concept_to_modal,
331
+ )
332
+ from .owl_manchester import (
333
+ parse_manchester, to_manchester, parse_manchester_axiom,
334
+ parse_manchester_role_axiom, role_axiom_to_manchester, ManchesterSyntaxError,
335
+ parse_manchester_data_range, to_manchester_data_range, parse_manchester_literal,
336
+ )
337
+ from .owl_functional import (
338
+ parse_owl_functional, parse_owl_functional_axioms, to_owl_functional,
339
+ parse_owl_functional_class_expression, to_owl_functional_class_expression,
340
+ OwlFunctionalResult, RefusedAxiom, ConsumedAxiom,
341
+ OwlFunctionalSyntaxError, OwlFunctionalUnsupportedError,
342
+ )
343
+ from .owl_reasoner import (
344
+ available as owl_reasoner_available,
345
+ external_concept_satisfiable, external_concept_unsatisfiable,
346
+ external_subsumes, external_equivalent,
347
+ external_abox_consistent, external_instance_check, external_instance_retrieval,
348
+ external_realize, external_realize_all,
349
+ OwlReasonerError,
350
+ )
351
+
352
+ __all__ = [
353
+ "Concept", "Top", "Bottom", "Atomic", "Not", "And", "Or", "Exists", "ForAll",
354
+ "AtLeast", "AtMost", "InverseRole", "Nominal", "HasValue", "nnf",
355
+ "DataExists", "DataForAll", "DataHasValue", "DataAtLeast", "DataAtMost",
356
+ "Literal", "DataRange", "Datatype", "DatatypeRestriction", "DataOneOf",
357
+ "DataComplementOf", "DataIntersectionOf", "DataUnionOf",
358
+ "UnsupportedDatatypeError", "OWL_THING", "OWL_DATA", "datarange_to_fol",
359
+ "TBox", "ABox",
360
+ "concept_satisfiable", "concept_unsatisfiable", "subsumes", "equivalent",
361
+ "abox_consistent",
362
+ "instance_check", "instance_retrieval", "realize", "realize_all",
363
+ "NonSimpleRoleError", "UnsupportedConceptError", "UnsupportedAxiomError",
364
+ "RoleExpressionError",
365
+ "classify", "Classification",
366
+ "parse_concept", "parse_gci", "ConceptSyntaxError",
367
+ "concept_to_fol", "subsumption_to_fol", "tbox_to_fol", "rbox_to_fol", "abox_to_fol",
368
+ "databox_to_fol", "data_sort_axioms", "check_kb_names",
369
+ "kb_to_fol", "KnowledgeBaseFOL", "SideAxiom", "RoleBoxOmittedError",
370
+ "concept_to_modal",
371
+ "parse_manchester", "to_manchester", "parse_manchester_axiom",
372
+ "parse_manchester_role_axiom", "role_axiom_to_manchester", "ManchesterSyntaxError",
373
+ "parse_manchester_data_range", "to_manchester_data_range", "parse_manchester_literal",
374
+ "parse_owl_functional", "parse_owl_functional_axioms", "to_owl_functional",
375
+ "parse_owl_functional_class_expression", "to_owl_functional_class_expression",
376
+ "OwlFunctionalResult", "RefusedAxiom", "ConsumedAxiom",
377
+ "OwlFunctionalSyntaxError", "OwlFunctionalUnsupportedError",
378
+ "owl_reasoner_available",
379
+ "external_concept_satisfiable", "external_concept_unsatisfiable",
380
+ "external_subsumes", "external_equivalent",
381
+ "external_abox_consistent", "external_instance_check", "external_instance_retrieval",
382
+ "external_realize", "external_realize_all",
383
+ "OwlReasonerError",
384
+ ]
@@ -0,0 +1,227 @@
1
+ """TBox classification: the named-concept subsumption hierarchy, as a pure reduction
2
+ to :func:`unicode_logic_kit.dl.tableau.subsumes`.
3
+
4
+ Given a TBox, :func:`classify` collects every named (:class:`~unicode_logic_kit.dl.concepts.Atomic`)
5
+ concept mentioned in it, decides the full pairwise subsumption matrix over those names
6
+ with the already-tested ``subsumes``, and turns that matrix into the structure an OWL
7
+ reasoner's classifier reports: equivalence classes (for concepts a TBox happens to make
8
+ synonyms), a transitively-reduced Hasse diagram of direct parents/children, and the
9
+ free transitive closure (``ancestors``). No new tableau completion rules are added —
10
+ correctness rides entirely on ``subsumes``'s existing soundness and completeness for ALC.
11
+
12
+ Public API: :class:`Classification`, :func:`classify`.
13
+ """
14
+
15
+ from dataclasses import dataclass
16
+ from typing import Dict, FrozenSet, Iterable, List, Optional, Set
17
+
18
+ from .concepts import (
19
+ Concept, Top, Bottom, Atomic, Not, And, Or, Exists, ForAll, AtLeast, AtMost,
20
+ InverseRole, Nominal, HasValue, DATA_CONCEPTS,
21
+ )
22
+ from .tableau import TBox, subsumes, _reject_beyond_alc, _reject_inputs, _reject_role_box
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class Classification:
27
+ """The named-concept subsumption hierarchy of a TBox (see :func:`classify`).
28
+
29
+ Every key and every member of every value is a *canonical name*: the
30
+ lexicographically smallest name in its mutual-subsumption equivalence class, so
31
+ that a TBox with synonyms (``Bachelor ≡ Man ⊓ ¬Married``) still reports one node
32
+ per distinct concept rather than one per name.
33
+ """
34
+
35
+ equivalents: Dict[str, FrozenSet[str]]
36
+ """Canonical name -> its full synonym set (itself included)."""
37
+
38
+ parents: Dict[str, FrozenSet[str]]
39
+ """Canonical name -> its *direct* super-concept names (the Hasse diagram)."""
40
+
41
+ children: Dict[str, FrozenSet[str]]
42
+ """Canonical name -> its *direct* sub-concept names (the Hasse diagram)."""
43
+
44
+ ancestors: Dict[str, FrozenSet[str]]
45
+ """Canonical name -> ALL (transitively closed) super-concept names."""
46
+
47
+
48
+ def _atomic_names(concept: Concept, names: Set[str]) -> None:
49
+ """Recursively collect every :class:`Atomic` name occurring in ``concept`` into ``names``.
50
+
51
+ Refuses — through the tableau's own concept guard, with
52
+ :class:`~unicode_logic_kit.dl.tableau.UnsupportedConceptError`, the refusal every
53
+ other entry point gives — a :class:`~unicode_logic_kit.dl.concepts.Nominal`
54
+ (caught directly: it is not "atomic-shaped" enough to fall through the
55
+ isinstance chain below, so it hits the final ``else`` on its own) or an
56
+ :class:`~unicode_logic_kit.dl.concepts.InverseRole`-valued ``role`` field
57
+ (checked EXPLICITLY in the ``Exists``/``ForAll``/``AtLeast``/``AtMost``
58
+ branch below, unlike :func:`unicode_logic_kit.dl.concepts.nnf`'s matching
59
+ branches, which leave ``role`` untyped on purpose — see that function's
60
+ docstring): without this explicit check, an ``InverseRole``-only axiom
61
+ would silently contribute NO names to :func:`classify`'s vocabulary
62
+ (``Exists``/``ForAll`` only ever recurse into ``.concept``, never inspect
63
+ ``.role``), so a TBox whose ONLY axiom uses one would classify as an empty,
64
+ vacuously "fine" hierarchy instead of raising — the one shape in this
65
+ module where :func:`unicode_logic_kit.dl.tableau.subsumes`'s own
66
+ ``_reject_beyond_alc`` guard would never even get called to catch it (see
67
+ :func:`classify`'s docstring: it never calls ``subsumes`` at all when its
68
+ collected vocabulary is empty).
69
+
70
+ A :class:`~unicode_logic_kit.dl.concepts.HasValue` and a data restriction are
71
+ refused through the tableau's own concept guard, with
72
+ :class:`~unicode_logic_kit.dl.tableau.UnsupportedConceptError`.
73
+ """
74
+ if isinstance(concept, Atomic):
75
+ names.add(concept.name)
76
+ elif isinstance(concept, (Top, Bottom)):
77
+ pass
78
+ elif isinstance(concept, DATA_CONCEPTS):
79
+ # A data restriction names a DATA property and a data range, never a
80
+ # class, so it contributes no name -- but it is outside the fragment the
81
+ # reduction to ``subsumes`` decides, and with a one-name vocabulary
82
+ # ``subsumes`` is never called to say so. The tableau's own concept
83
+ # guard raises the SAME refusal every other entry point gives.
84
+ _reject_beyond_alc(concept)
85
+ elif isinstance(concept, HasValue):
86
+ # Contributes NO class name -- a value restriction names a ROLE and an
87
+ # INDIVIDUAL, neither of which belongs in a subsumption hierarchy over
88
+ # named CONCEPTS -- but it is a nominal in disguise, which the reduction
89
+ # to ``subsumes`` does not decide, and with a one-name vocabulary
90
+ # ``subsumes`` is never called to say so. The tableau's own concept
91
+ # guard raises the SAME refusal every other entry point gives.
92
+ _reject_beyond_alc(concept)
93
+ elif isinstance(concept, Not):
94
+ _atomic_names(concept.concept, names)
95
+ elif isinstance(concept, (And, Or)):
96
+ _atomic_names(concept.left, names)
97
+ _atomic_names(concept.right, names)
98
+ elif isinstance(concept, (Exists, ForAll, AtLeast, AtMost)):
99
+ if isinstance(concept.role, InverseRole):
100
+ # An inverse role contributes no class name either, and with a
101
+ # one-name vocabulary ``subsumes`` is never called to refuse it. The
102
+ # tableau's own concept guard raises the SAME refusal every other
103
+ # entry point gives (UnsupportedConceptError, which the MCP tools
104
+ # return as a structured error; a bare TypeError escaped them).
105
+ _reject_beyond_alc(concept)
106
+ _atomic_names(concept.concept, names)
107
+ else:
108
+ # A bare Nominal lands here: refused by the same guard, by name.
109
+ _reject_beyond_alc(concept)
110
+ raise TypeError(f"classify: unsupported concept {type(concept).__name__}")
111
+
112
+
113
+ def _equivalence_classes(names: List[str], holds) -> List[Set[str]]:
114
+ """Connected components of the mutual-subsumption graph over ``names``."""
115
+ adjacency: Dict[str, Set[str]] = {n: set() for n in names}
116
+ for a in names:
117
+ for b in names:
118
+ if a != b and holds[(a, b)] and holds[(b, a)]:
119
+ adjacency[a].add(b)
120
+ adjacency[b].add(a)
121
+ seen: Set[str] = set()
122
+ classes: List[Set[str]] = []
123
+ for n in names:
124
+ if n in seen:
125
+ continue
126
+ stack, component = [n], set()
127
+ while stack:
128
+ x = stack.pop()
129
+ if x in component:
130
+ continue
131
+ component.add(x)
132
+ stack.extend(adjacency[x] - component)
133
+ seen |= component
134
+ classes.append(component)
135
+ return classes
136
+
137
+
138
+ def classify(tbox: TBox, concepts: Optional[Iterable[Concept]] = None) -> Classification:
139
+ """Classify every named concept of ``tbox`` into a subsumption hierarchy.
140
+
141
+ The vocabulary is every :class:`Atomic` name reachable from ``tbox.inclusions``
142
+ (walked through ``And``/``Or``/``Not``/``Exists``/``ForAll``), extended with the
143
+ names occurring in ``concepts`` when given — useful for a name of interest that
144
+ never appears in an axiom (and so would otherwise be invisible, an isolated node
145
+ with no parents/children/ancestors and a singleton equivalence class).
146
+
147
+ Raises:
148
+ ~unicode_logic_kit.dl.tableau.UnsupportedAxiomError:
149
+ ``tbox`` carries an axiom KIND no in-house rule
150
+ decides, exactly as ``subsumes`` would raise -- including for a
151
+ TBox with ONE named concept, where the pair loop below never calls
152
+ ``subsumes`` at all (its only ordered pair is ``(A, A)``, answered
153
+ ``True`` without a tableau), so without the up-front guard that one
154
+ shape returned a hierarchy for a knowledge base every other entry
155
+ point refuses.
156
+ ~unicode_logic_kit.dl.tableau.UnsupportedConceptError:
157
+ a stored class expression (or one of
158
+ ``concepts``) contains a value restriction (``HasValue``, a
159
+ nominal in disguise) or a data restriction -- raised by the
160
+ tableau's concept guard whatever the vocabulary's size.
161
+ """
162
+ # The axiom-level guard, once, before anything is decided -- the same call
163
+ # concept_satisfiable/abox_consistent open with. A TBox with zero or one
164
+ # named concept never reaches `subsumes`, so inheriting it was not enough.
165
+ _reject_role_box(tbox, None)
166
+ names: Set[str] = set()
167
+ for sub, sup in tbox.inclusions:
168
+ _atomic_names(sub, names)
169
+ _atomic_names(sup, names)
170
+ extra = list(concepts) if concepts is not None else []
171
+ for c in extra:
172
+ _atomic_names(c, names)
173
+ # The concept-level half of the same guard, over EVERY class expression the
174
+ # TBox stores (a domain or range filler included -- `_atomic_names` above
175
+ # reads inclusions only) and over the extra concepts. `subsumes` runs it per
176
+ # pair, but a vocabulary of fewer than two names makes no such call, and a
177
+ # knowledge base that every other entry point refuses (an OWL 2 built-in
178
+ # property name as a restriction's role, a nominal in a domain filler) got a
179
+ # hierarchy here.
180
+ _reject_inputs(tbox, None, extra)
181
+ ordered = sorted(names)
182
+
183
+ # The O(n^2) reduction: decide sub/sup for every ordered pair with `subsumes`.
184
+ holds = {}
185
+ for a in ordered:
186
+ for b in ordered:
187
+ holds[(a, b)] = True if a == b else subsumes(Atomic(a), Atomic(b), tbox)
188
+
189
+ classes = _equivalence_classes(ordered, holds)
190
+ canon_of: Dict[str, str] = {}
191
+ equivalents: Dict[str, FrozenSet[str]] = {}
192
+ for component in classes:
193
+ canon = min(component)
194
+ equivalents[canon] = frozenset(component)
195
+ for member in component:
196
+ canon_of[member] = canon
197
+ canon_names = sorted(equivalents)
198
+
199
+ # Free byproduct: the un-reduced transitive closure, at the canonical-name level
200
+ # (equivalence-class members all agree on what they subsume/are subsumed by, so one
201
+ # representative pairwise lookup per class is enough).
202
+ ancestors: Dict[str, Set[str]] = {c: set() for c in canon_names}
203
+ for a in canon_names:
204
+ for b in canon_names:
205
+ if a != b and holds[(a, b)]:
206
+ ancestors[a].add(b)
207
+
208
+ # Transitive reduction (Hasse diagram): drop a -> b when some c also ancestor of a
209
+ # already reaches b, i.e. a -> c -> b makes a -> b redundant.
210
+ parents: Dict[str, FrozenSet[str]] = {}
211
+ for a in canon_names:
212
+ direct = set(ancestors[a])
213
+ for c in ancestors[a]:
214
+ direct -= ancestors[c]
215
+ parents[a] = frozenset(direct)
216
+
217
+ children: Dict[str, Set[str]] = {c: set() for c in canon_names}
218
+ for a, ps in parents.items():
219
+ for p in ps:
220
+ children[p].add(a)
221
+
222
+ return Classification(
223
+ equivalents=equivalents,
224
+ parents=parents,
225
+ children={c: frozenset(s) for c, s in children.items()},
226
+ ancestors={c: frozenset(s) for c, s in ancestors.items()},
227
+ )