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,730 @@
1
+ """Building the ILP task: :class:`Example`, :class:`IlpTask`, and the emitter.
2
+
3
+ The three files Popper reads, from structures the kit already has:
4
+
5
+ ``bk.pl``
6
+ the background knowledge — one Prolog fact per tuple in a structure's
7
+ extension, with individuals renamed so a name pins down its own example.
8
+ ``exs.pl``
9
+ ``pos(target(e)).`` / ``neg(target(e)).``, one line per example.
10
+ ``bias.pl``
11
+ ``head_pred`` / ``body_pred`` declarations plus the size bounds.
12
+
13
+ :meth:`IlpTask.write_aleph` emits Aleph's own, genuinely different, three-file
14
+ layout (``<stem>.b``/``.f``/``.n``) instead — see :meth:`IlpTask.aleph_bias_text`
15
+ and :meth:`IlpTask.aleph_examples_text` for what changes and why. Everything
16
+ below this point — the encoding checks, the individual-naming scheme, the
17
+ background facts themselves — is shared between both learners; only the bias
18
+ and example RENDERINGS are learner-specific.
19
+
20
+ Every check that the EXAMPLES and the VOCABULARY can be encoded soundly runs in
21
+ :meth:`IlpTask.__post_init__` — before a file exists, so a task that has been
22
+ constructed is a task whose encoding has been checked. Two refusals necessarily
23
+ come later, because they are not about the encoding: materialising a computed
24
+ predicate is only costed when the facts are actually rendered, and a missing
25
+ parent directory is only knowable at :meth:`IlpTask.write`. Both leave nothing
26
+ behind. See the package docstring for what the encoding checks are and which
27
+ mistakes they were built from.
28
+
29
+ **Facts are the extension, exactly.** One Prolog fact per tuple, no
30
+ orientation guessed, nothing added. A symmetric relation (a chemical bond) is
31
+ stored symmetrically by
32
+ :func:`~unicode_logic_kit.chem.mol_to_structure` and therefore emitted in both
33
+ directions; an asymmetric one is emitted in the one direction it has. The
34
+ emitter has no opinion about which it is looking at, which is the only way it
35
+ can be right about both.
36
+
37
+ **Computed predicates are materialised.** A
38
+ :class:`~unicode_logic_kit.semantics.structures.FiniteStructure` may decide a
39
+ predicate by callable rather than by stored extension (``in_ring`` and friends
40
+ — see that module). Prolog needs facts, so the callable is asked about every
41
+ tuple. That costs ``|domain| ** arity`` calls, which is fine at arity 1–2 and
42
+ explosive above; the cost is checked against ``max_computed_tuples`` and
43
+ REFUSED rather than silently paid.
44
+ """
45
+
46
+ import io
47
+ import os
48
+ import re
49
+ from dataclasses import dataclass, field
50
+ from itertools import product
51
+ from typing import (
52
+ Dict, Iterable, List, Mapping, Optional, Sequence, Tuple, Union,
53
+ )
54
+
55
+ from ..semantics.structures import FiniteStructure
56
+
57
+ __all__ = [
58
+ "IlpEncodingError", "Example", "IlpTask", "task_from_structures",
59
+ "to_prolog_atom",
60
+ ]
61
+
62
+ #: A Prolog atom: lower-case initial, then word characters. Quoted atoms
63
+ #: (``'1,2-diacyl'``) are deliberately NOT produced — a quoted name survives
64
+ #: into the learner's output and back through
65
+ #: :func:`~unicode_logic_kit.fol.parse_prolog_clause` as a token no kit grammar
66
+ #: accepts, so the caller is asked to rename instead.
67
+ #:
68
+ #: Matched with ``fullmatch``, never ``match``: Python's ``$`` also matches
69
+ #: immediately before a trailing newline, so ``"m1\n"`` would pass — and in
70
+ #: Prolog a newline is whitespace, which would make ``m1`` and ``m1\n`` ONE
71
+ #: atom while every uniqueness check here still saw two. That is exactly the
72
+ #: collision this module exists to prevent, arriving through the checker.
73
+ _PROLOG_ATOM_RE = re.compile(r"[a-z][A-Za-z0-9_]*")
74
+
75
+ #: The tail of an individual's name, after the example prefix has supplied a
76
+ #: legal initial character. Same ``fullmatch`` discipline.
77
+ _INDIVIDUAL_TAIL_RE = re.compile(r"[A-Za-z0-9_]+")
78
+
79
+ Key = Tuple[str, int]
80
+
81
+
82
+ class IlpEncodingError(ValueError):
83
+ """A task that cannot be encoded soundly.
84
+
85
+ Subclasses :class:`ValueError` so ``except ValueError`` still catches it,
86
+ matching :func:`~unicode_logic_kit.eval.check_definitions`, which raises the
87
+ same class for the same kind of problem: this is CONFIGURATION, not data.
88
+ A campaign that cannot encode its task must stop, not emit rows.
89
+ """
90
+
91
+
92
+ def to_prolog_atom(name: str) -> str:
93
+ """The Prolog spelling of a kit name — first character folded down.
94
+
95
+ Only the FIRST character, never the whole name: ``bSINGLE`` is already a
96
+ legal Prolog atom and folding it to ``bsingle`` throws away a distinction
97
+ ChemLog's vocabulary makes, which then cannot be restored on the way back
98
+ (:func:`~unicode_logic_kit.fol.parse_prolog_clause` would return
99
+ ``Bsingle``). The same rule, in the opposite direction, as
100
+ :mod:`unicode_logic_kit.fol.prolog_input`.
101
+
102
+ Raises:
103
+ IlpEncodingError: the folded name is not a legal unquoted Prolog atom.
104
+
105
+ Example:
106
+ >>> from unicode_logic_kit.ilp import to_prolog_atom
107
+ >>> to_prolog_atom("Amide"), to_prolog_atom("bSINGLE")
108
+ ('amide', 'bSINGLE')
109
+ """
110
+ folded = name[:1].lower() + name[1:] if name else name
111
+ if not _PROLOG_ATOM_RE.fullmatch(folded):
112
+ raise IlpEncodingError(
113
+ f"to_prolog_atom: {name!r} does not fold to a legal Prolog atom "
114
+ f"(got {folded!r}; needs to match {_PROLOG_ATOM_RE.pattern}). "
115
+ "Rename it — quoting would hide the problem until the learner's "
116
+ "output came back unparseable.")
117
+ return folded
118
+
119
+
120
+ @dataclass(frozen=True)
121
+ class Example:
122
+ """One labelled structure: the thing the learner sees as ``pos``/``neg``.
123
+
124
+ Args:
125
+ id: names this example in the emitted Prolog. Folded to an atom by
126
+ :func:`to_prolog_atom`, so ``"M1"`` and ``"m1"`` are the SAME
127
+ example name and :class:`IlpTask` will refuse both at once.
128
+ structure: what the background knowledge is read off.
129
+ label: ``True`` for a positive example, ``False`` for a negative one.
130
+ note: free text carried into the emitted files as a comment — a
131
+ SMILES, an accession, whatever makes the facts readable next to
132
+ their source. Never parsed.
133
+ """
134
+
135
+ id: str
136
+ structure: FiniteStructure
137
+ label: bool
138
+ note: str = ""
139
+
140
+ def __post_init__(self):
141
+ to_prolog_atom(self.id) # raises with a message if illegal
142
+ if not isinstance(self.structure, FiniteStructure):
143
+ raise IlpEncodingError(
144
+ f"Example {self.id!r}: structure must be a FiniteStructure, "
145
+ f"got {type(self.structure).__name__}")
146
+ if any(character in self.note for character in "\n\r"):
147
+ raise IlpEncodingError(
148
+ f"Example {self.id!r}: note must be a single line — it is "
149
+ "emitted as a Prolog comment, and a bare CR would put a CRLF "
150
+ "into a file this module promises is LF-only")
151
+
152
+ @property
153
+ def atom(self) -> str:
154
+ """The example's name as it appears in Prolog."""
155
+ return to_prolog_atom(self.id)
156
+
157
+
158
+ def _comment(text: str) -> str:
159
+ return f"% {text}"
160
+
161
+
162
+ @dataclass(frozen=True)
163
+ class IlpTask:
164
+ """A complete ILP task, checked at construction and rendered on demand.
165
+
166
+ Args:
167
+ target: the predicate to be learned, arity 1 (its single argument is
168
+ the example). Written ``amide`` or ``Amide`` — folded either way.
169
+ examples: the labelled structures. At least one of each label: with no
170
+ positives there is nothing to learn, and with no negatives the
171
+ empty body is already a perfect hypothesis.
172
+ body_predicates: the vocabulary offered to the learner, as
173
+ ``(name, arity)`` pairs in the structures' own spelling. ``None``
174
+ infers it from the structures — convenient, but every extra
175
+ predicate multiplies the search space, so naming the handful you
176
+ mean is usually the better task.
177
+ membership: the ONE predicate carrying the example argument. Its facts
178
+ say "this individual belongs to that example" and nothing else.
179
+ max_vars, max_body, max_clauses: Popper's size bounds, emitted into
180
+ ``bias.pl``.
181
+ extra_bias: additional bias lines, verbatim, one per element (e.g.
182
+ ``"enable_recursion."`` or a ``type`` declaration). Not validated
183
+ — the learner is the authority on its own bias language.
184
+ max_computed_tuples: refuse to materialise a computed predicate whose
185
+ enumeration would cost more than this many calls.
186
+
187
+ Raises:
188
+ IlpEncodingError: any of the above is violated, or two
189
+ ``(example, individual)`` pairs would produce the same Prolog
190
+ constant.
191
+ """
192
+
193
+ target: str
194
+ examples: Tuple[Example, ...]
195
+ body_predicates: Optional[Tuple[Key, ...]] = None
196
+ membership: str = "atom_in"
197
+ max_vars: int = 6
198
+ max_body: int = 8
199
+ max_clauses: int = 1
200
+ extra_bias: Tuple[str, ...] = ()
201
+ max_computed_tuples: int = 100_000
202
+
203
+ #: Set during validation: ``(name, arity)`` pairs an INFERRED vocabulary
204
+ #: left out, with the reason. Empty when ``body_predicates`` was given.
205
+ #: Not a constructor argument — it is a finding, not a setting.
206
+ excluded_predicates: Tuple[Tuple[Key, str], ...] = field(
207
+ default=(), compare=False, init=False)
208
+
209
+ def __post_init__(self):
210
+ object.__setattr__(self, "examples", tuple(self.examples))
211
+ if isinstance(self.extra_bias, str):
212
+ raise IlpEncodingError(
213
+ "IlpTask: extra_bias must be a sequence of lines, not a "
214
+ "single string — a string is iterable, so it would become one "
215
+ "bias line per character")
216
+ object.__setattr__(self, "extra_bias", tuple(self.extra_bias))
217
+ to_prolog_atom(self.target)
218
+ membership_atom = to_prolog_atom(self.membership)
219
+
220
+ if not self.examples:
221
+ raise IlpEncodingError("IlpTask: no examples")
222
+ positives = [e for e in self.examples if e.label]
223
+ negatives = [e for e in self.examples if not e.label]
224
+ if not positives:
225
+ raise IlpEncodingError(
226
+ "IlpTask: no positive examples — there is nothing to learn")
227
+ if not negatives:
228
+ raise IlpEncodingError(
229
+ "IlpTask: no negative examples — with none, the empty body is "
230
+ "already a perfect hypothesis and any result would be "
231
+ "vacuous. Add negatives, or use a different tool.")
232
+
233
+ seen_ids: Dict[str, str] = {}
234
+ for example in self.examples:
235
+ atom = example.atom
236
+ if atom in seen_ids:
237
+ raise IlpEncodingError(
238
+ f"IlpTask: examples {seen_ids[atom]!r} and {example.id!r} "
239
+ f"both name the example {atom!r} in Prolog")
240
+ seen_ids[atom] = example.id
241
+
242
+ for bound, name in ((self.max_vars, "max_vars"),
243
+ (self.max_body, "max_body"),
244
+ (self.max_clauses, "max_clauses")):
245
+ if not isinstance(bound, int) or bound < 1:
246
+ raise IlpEncodingError(
247
+ f"IlpTask: {name} must be a positive integer, got {bound!r}")
248
+ if not isinstance(self.max_computed_tuples, int) or \
249
+ self.max_computed_tuples < 1:
250
+ raise IlpEncodingError(
251
+ "IlpTask: max_computed_tuples must be a positive integer, got "
252
+ f"{self.max_computed_tuples!r}")
253
+
254
+ if self.body_predicates is None:
255
+ inferred, excluded = self._infer_vocabulary(membership_atom)
256
+ object.__setattr__(self, "body_predicates", inferred)
257
+ object.__setattr__(self, "excluded_predicates", excluded)
258
+ else:
259
+ object.__setattr__(
260
+ self, "body_predicates",
261
+ tuple(sorted({(str(n), int(a)) for n, a in self.body_predicates})))
262
+ object.__setattr__(self, "excluded_predicates", ())
263
+
264
+ functors: Dict[Key, str] = {}
265
+ for name, arity in self.body_predicates:
266
+ atom = to_prolog_atom(name)
267
+ if arity >= 1:
268
+ # Only the first character is folded, so `Ring/1` and `ring/1`
269
+ # are two symbols and one Prolog predicate. Emitted, their
270
+ # extensions would be unioned under that one functor —
271
+ # background knowledge describing a relation that exists in no
272
+ # structure. Same class of collision as two individuals
273
+ # spelling one constant, and refused for the same reason.
274
+ if functors.get((atom, arity), name) != name:
275
+ raise IlpEncodingError(
276
+ f"IlpTask: {functors[(atom, arity)]}/{arity} and "
277
+ f"{name}/{arity} both emit as {atom}/{arity}. Their "
278
+ "facts would be merged into one relation the "
279
+ "structures do not have — rename one of the "
280
+ "structure's symbols.")
281
+ functors[(atom, arity)] = name
282
+ if arity < 1:
283
+ raise IlpEncodingError(
284
+ f"IlpTask: body predicate {name}/{arity} is 0-ary. A "
285
+ "0-ary fact carries no individual, so it cannot be "
286
+ "attached to one example — emitted, it would hold across "
287
+ "every example at once. Model it as a unary predicate of "
288
+ "the example's individuals instead.")
289
+ if atom == membership_atom:
290
+ raise IlpEncodingError(
291
+ f"IlpTask: {name}/{arity} collides with the membership "
292
+ f"predicate {membership_atom!r}. The example argument "
293
+ "must sit on exactly one predicate; rename either the "
294
+ "structure's symbol or membership=.")
295
+ if not any(e.structure.interprets(name, arity)
296
+ for e in self.examples):
297
+ raise IlpEncodingError(
298
+ f"IlpTask: body predicate {name}/{arity} is interpreted by "
299
+ "no example structure — every clause using it would be "
300
+ "unsatisfiable, so offering it can only waste search")
301
+
302
+ self._individual_names() # raises on a name collision
303
+
304
+ # -- vocabulary ---------------------------------------------------------
305
+
306
+ def _infer_vocabulary(self, membership_atom: str
307
+ ) -> Tuple[Tuple[Key, ...], Tuple[Tuple[Key, str], ...]]:
308
+ """Every symbol the structures interpret, minus the ones that cannot
309
+ be encoded soundly — each recorded with its reason rather than
310
+ dropped silently."""
311
+ symbols = sorted({key for e in self.examples
312
+ for key in e.structure.signature()})
313
+ keep: List[Key] = []
314
+ excluded: List[Tuple[Key, str]] = []
315
+ for name, arity in symbols:
316
+ if arity < 1:
317
+ excluded.append(((name, arity),
318
+ "0-ary: cannot be attached to one example"))
319
+ continue
320
+ try:
321
+ atom = to_prolog_atom(name)
322
+ except IlpEncodingError:
323
+ excluded.append(((name, arity),
324
+ "does not fold to a legal Prolog atom"))
325
+ continue
326
+ if atom == membership_atom:
327
+ excluded.append(((name, arity),
328
+ "collides with the membership predicate"))
329
+ continue
330
+ keep.append((name, arity))
331
+ return tuple(keep), tuple(excluded)
332
+
333
+ # -- naming -------------------------------------------------------------
334
+
335
+ def individual_name(self, example: Example, individual: str) -> str:
336
+ """The Prolog constant for ``individual`` inside ``example``.
337
+
338
+ ``<example>_<individual>`` — the whole point of the module's first
339
+ guarantee. Uniqueness of the result across the task is checked once,
340
+ in the constructor.
341
+ """
342
+ if not _INDIVIDUAL_TAIL_RE.fullmatch(individual):
343
+ raise IlpEncodingError(
344
+ f"IlpTask: example {example.id!r} has an individual "
345
+ f"{individual!r} that is not word characters — it cannot be "
346
+ "part of an unquoted Prolog atom. Rename the structure's "
347
+ "domain.")
348
+ return f"{example.atom}_{individual}"
349
+
350
+ def _individual_names(self) -> Dict[Tuple[str, str], str]:
351
+ """``(example atom, individual) -> Prolog constant``, checked injective.
352
+
353
+ Injective over EVERY constant the task will emit — the individuals and
354
+ the example names together, because they share one namespace in the
355
+ file. Both collisions are real and reachable:
356
+
357
+ * example ``m1`` with individual ``a_b`` and example ``m1_a`` with
358
+ individual ``b`` both spell ``m1_a_b``;
359
+ * example ``m1`` with individual ``a`` spells ``m1_a``, which is also
360
+ the name of an example called ``m1_a``.
361
+
362
+ In either case a learner handed that file can join two examples
363
+ through one constant — the exact failure the prefixing exists to
364
+ prevent.
365
+ """
366
+ names: Dict[Tuple[str, str], str] = {}
367
+ owners: Dict[str, str] = {
368
+ example.atom: f"the example {example.id!r}"
369
+ for example in self.examples
370
+ }
371
+ for example in self.examples:
372
+ for individual in example.structure.domain:
373
+ name = self.individual_name(example, individual)
374
+ mine = f"individual {individual!r} of example {example.id!r}"
375
+ if name in owners:
376
+ raise IlpEncodingError(
377
+ f"IlpTask: {mine} and {owners[name]} both spell "
378
+ f"{name!r} in Prolog. One constant for two things is "
379
+ "exactly what the example prefix exists to prevent — "
380
+ "rename an example.")
381
+ owners[name] = mine
382
+ names[(example.atom, individual)] = name
383
+ return names
384
+
385
+ # -- facts --------------------------------------------------------------
386
+
387
+ def _rows(self, structure: FiniteStructure, name: str, arity: int
388
+ ) -> List[Tuple[str, ...]]:
389
+ """The extension of ``name/arity`` in ``structure``, sorted.
390
+
391
+ Materialises a computed predicate by asking it about every tuple —
392
+ the only thing Prolog can be given — after checking the cost.
393
+ """
394
+ key = (name, arity)
395
+ if key in structure.extensions:
396
+ return sorted(structure.extensions[key])
397
+ cost = len(structure.domain) ** arity
398
+ if cost > self.max_computed_tuples:
399
+ raise IlpEncodingError(
400
+ f"IlpTask: materialising the computed predicate {name}/{arity} "
401
+ f"over a domain of {len(structure.domain)} would cost {cost} "
402
+ f"calls, over max_computed_tuples={self.max_computed_tuples}. "
403
+ "Raise the bound, or leave this predicate out of "
404
+ "body_predicates.")
405
+ decide = structure.computed[key]
406
+ return sorted(tuple(t) for t in product(structure.domain, repeat=arity)
407
+ if decide(*t))
408
+
409
+ # -- rendering ----------------------------------------------------------
410
+
411
+ def background_text(self) -> str:
412
+ """``bk.pl``: the membership facts and every background fact.
413
+
414
+ Deterministic: examples in the order given, symbols sorted, tuples
415
+ sorted. The same task renders byte-identically every time, so a task
416
+ directory can be diffed and committed.
417
+ """
418
+ membership = to_prolog_atom(self.membership)
419
+ lines: List[str] = [
420
+ _comment(f"background knowledge for {to_prolog_atom(self.target)}/1"),
421
+ _comment(f"{len(self.examples)} examples; the example argument "
422
+ f"lives on {membership}/2 alone"),
423
+ ]
424
+ for (name, arity), reason in self.excluded_predicates:
425
+ lines.append(_comment(f"left out: {name}/{arity} — {reason}"))
426
+ lines.append("")
427
+ lines.append(f":- discontiguous {membership}/2.")
428
+ for name, arity in self.body_predicates:
429
+ lines.append(f":- discontiguous {to_prolog_atom(name)}/{arity}.")
430
+
431
+ names = self._individual_names()
432
+ for example in self.examples:
433
+ label = "pos" if example.label else "neg"
434
+ header = f"{example.atom} ({label})"
435
+ if example.note:
436
+ header += f": {example.note}"
437
+ lines.append("")
438
+ lines.append(_comment(header))
439
+ for individual in example.structure.domain:
440
+ constant = names[(example.atom, individual)]
441
+ lines.append(f"{membership}({example.atom},{constant}).")
442
+ for name, arity in self.body_predicates:
443
+ if not example.structure.interprets(name, arity):
444
+ continue
445
+ functor = to_prolog_atom(name)
446
+ for row in self._rows(example.structure, name, arity):
447
+ args = ",".join(names[(example.atom, i)] for i in row)
448
+ lines.append(f"{functor}({args}).")
449
+ return "\n".join(lines) + "\n"
450
+
451
+ def examples_text(self) -> str:
452
+ """``exs.pl``: one ``pos``/``neg`` line per example."""
453
+ target = to_prolog_atom(self.target)
454
+ lines = []
455
+ for example in self.examples:
456
+ label = "pos" if example.label else "neg"
457
+ line = f"{label}({target}({example.atom}))."
458
+ if example.note:
459
+ line += f" {_comment(example.note)}"
460
+ lines.append(line)
461
+ return "\n".join(lines) + "\n"
462
+
463
+ def aleph_examples_text(self, label: bool) -> str:
464
+ """Aleph's ``.f``/``.n`` convention: bare ground atoms, one per line.
465
+
466
+ ``target(e).``, one line per example of the given ``label`` — no
467
+ ``pos(...)``/``neg(...)`` wrapper, genuinely different from
468
+ :meth:`examples_text`: Aleph's positive and negative examples live in
469
+ two SEPARATE files (``.f`` and ``.n``), so the label is which file the
470
+ text goes into rather than which functor wraps the atom. Call it
471
+ twice, ``True`` then ``False``, for the two halves — exactly what
472
+ :meth:`write_aleph` does.
473
+ """
474
+ target = to_prolog_atom(self.target)
475
+ lines = []
476
+ for example in self.examples:
477
+ if example.label != label:
478
+ continue
479
+ line = f"{target}({example.atom})."
480
+ if example.note:
481
+ line += f" {_comment(example.note)}"
482
+ lines.append(line)
483
+ return "\n".join(lines) + "\n"
484
+
485
+ def bias_text(self) -> str:
486
+ """``bias.pl``: the head and body declarations plus the size bounds."""
487
+ lines = [f"head_pred({to_prolog_atom(self.target)},1).",
488
+ f"body_pred({to_prolog_atom(self.membership)},2)."]
489
+ for name, arity in self.body_predicates:
490
+ lines.append(f"body_pred({to_prolog_atom(name)},{arity}).")
491
+ lines += [f"max_vars({self.max_vars}).",
492
+ f"max_body({self.max_body}).",
493
+ f"max_clauses({self.max_clauses})."]
494
+ lines += list(self.extra_bias)
495
+ return "\n".join(lines) + "\n"
496
+
497
+ def aleph_bias_text(self) -> str:
498
+ """Aleph's mode/determination bias — the alternative to :meth:`bias_text`.
499
+
500
+ Aleph's ``modeh``/``modeb`` declarations carry a per-argument mode
501
+ (``+Type`` bound, ``-Type`` free, ``#Type`` ground) that a real user
502
+ derives from what each argument of each predicate actually MEANS.
503
+ :class:`FiniteStructure` has no such per-argument sort to read that
504
+ from — every argument is just "an individual" — so this method
505
+ applies one fixed, documented CONVENTION instead of inferring
506
+ anything: every body predicate's first argument is bound
507
+ (``+individual``) and every remaining argument is free
508
+ (``-individual``); a unary predicate gets ``+individual`` alone. This
509
+ happens to be exactly right for a functional-style fact
510
+ (``c(X)`` — "X is a carbon") and merely usable, not meaningful, for a
511
+ genuinely relational one (``bDOUBLE(X,Y)`` carries no sense in which
512
+ one endpoint is more "input" than the other) — stated here rather
513
+ than silently presented as if the mode meant something it does not.
514
+ The head is always ``modeh(1, target(+example))`` (:attr:`target` is
515
+ arity 1 by construction) and the membership predicate gets its own
516
+ hand-written line, since its first argument is the example rather
517
+ than an individual: ``modeb(*, membership(+example,-individual))``.
518
+ Every ``modeh``/``modeb`` recall is left unbounded (``1`` for the
519
+ head, since exactly one head atom is ever matched per example;
520
+ ``*`` for every body literal), matching how Popper's own bias carries
521
+ no recall bound at all.
522
+
523
+ One ``determination(target/1, pred/arity)`` line follows per body
524
+ predicate, membership included — Aleph's other half of the bias,
525
+ telling it which predicates may appear in a clause for ``target``
526
+ at all.
527
+
528
+ :attr:`max_body` is the one Popper size bound with a real Aleph
529
+ analog (the number of literals in a clause body) and becomes
530
+ ``:- set(clauselength, N).``. :attr:`max_vars` and :attr:`max_clauses`
531
+ do NOT: Aleph has no per-clause bound on distinct variables, and its
532
+ clause count comes from the outer covering loop of ``induce/0``
533
+ (repeatedly saturate-and-reduce on whatever positive examples are
534
+ still uncovered), not from anything written into a bias file. Rather
535
+ than drop them or fake an equivalent, both are named in a comment so
536
+ a reader checking this file against the ``IlpTask`` that produced it
537
+ finds an explanation, not a silent gap.
538
+
539
+ :attr:`extra_bias` is appended verbatim, exactly as in
540
+ :meth:`bias_text` — the field is deliberately learner-agnostic (its
541
+ own docstring's example, a ``type(...)`` declaration, is itself
542
+ Aleph syntax) and unvalidated, since the learner is the authority on
543
+ its own bias language.
544
+
545
+ This mode/determination bias, together with :meth:`background_text`
546
+ as Aleph's combined background-and-bias file, was checked against a
547
+ real Aleph (the SWI-Prolog ``aleph`` pack, ``aleph_orig.pl``): the
548
+ emitted ``.b``/``.f``/``.n`` triple loads without a type/1 fact of any
549
+ kind — ``+example``/``-individual`` bind purely from resolving the
550
+ actual background predicates during saturation, never from
551
+ enumerating a declared type — and ``induce`` learns the intended
552
+ target clause. See ``tests/test_ilp_aleph.py`` for the harness (it
553
+ skips, rather than fails, where Aleph is not installed).
554
+ """
555
+ target = to_prolog_atom(self.target)
556
+ membership = to_prolog_atom(self.membership)
557
+ lines = [f":- modeh(1, {target}(+example))."]
558
+ lines.append(f":- modeb(*, {membership}(+example,-individual)).")
559
+ for name, arity in self.body_predicates:
560
+ atom = to_prolog_atom(name)
561
+ args = ",".join(["+individual"] + ["-individual"] * (arity - 1))
562
+ lines.append(f":- modeb(*, {atom}({args})).")
563
+ lines.append(f":- determination({target}/1, {membership}/2).")
564
+ for name, arity in self.body_predicates:
565
+ lines.append(
566
+ f":- determination({target}/1, {to_prolog_atom(name)}/{arity}).")
567
+ lines.append(
568
+ f"% max_vars({self.max_vars}) and max_clauses({self.max_clauses}) "
569
+ "have no single-clause Aleph equivalent: Aleph's clause count "
570
+ "comes from its own covering loop (induce/0), not a bound in "
571
+ "this file, and max_vars bounds a Popper-specific search Aleph "
572
+ "does not expose per clause.")
573
+ lines.append(f":- set(clauselength, {self.max_body}).")
574
+ lines += list(self.extra_bias)
575
+ return "\n".join(lines) + "\n"
576
+
577
+ def write(self, directory: str) -> Dict[str, str]:
578
+ """Write ``bk.pl``, ``exs.pl`` and ``bias.pl`` into ``directory``.
579
+
580
+ The directory itself is created; its PARENT must already exist, so a
581
+ typo in a long path fails here instead of quietly building a new tree
582
+ somewhere nobody will look. For the same reason all three files are
583
+ RENDERED before the directory is created: a task whose rendering is
584
+ refused (a computed predicate over ``max_computed_tuples``) leaves
585
+ nothing behind at all.
586
+
587
+ Files are written with ``\\n`` line endings on every platform — Prolog
588
+ reads either, but a task directory that differs between Windows and
589
+ Linux is a task directory that cannot be diffed.
590
+
591
+ Returns:
592
+ ``{"bk": path, "exs": path, "bias": path}``.
593
+ """
594
+ parent = os.path.dirname(os.path.abspath(directory))
595
+ if not os.path.isdir(parent):
596
+ raise IlpEncodingError(
597
+ f"IlpTask.write: the parent directory {parent!r} does not "
598
+ "exist")
599
+ rendered = (("bk", self.background_text()),
600
+ ("exs", self.examples_text()),
601
+ ("bias", self.bias_text()))
602
+ os.makedirs(directory, exist_ok=True)
603
+ paths = {}
604
+ for stem, text in rendered:
605
+ path = os.path.join(directory, f"{stem}.pl")
606
+ with io.open(path, "w", encoding="utf-8", newline="\n") as handle:
607
+ handle.write(text)
608
+ paths[stem] = path
609
+ return paths
610
+
611
+ def write_aleph(self, directory: str, filestem: str = "task") -> Dict[str, str]:
612
+ """Write Aleph's ``<filestem>.b``/``.f``/``.n`` into ``directory``.
613
+
614
+ Aleph's own file layout, parallel to :meth:`write`'s Popper layout
615
+ but genuinely different, not just renamed: ``<filestem>.b`` carries
616
+ BOTH the background facts and the mode/determination bias — Aleph
617
+ reads background knowledge and bias declarations from the same file
618
+ — so it is :meth:`background_text` and :meth:`aleph_bias_text`
619
+ concatenated (with a blank line between the two sections), where
620
+ :meth:`write` keeps those in two separate files (``bk.pl``,
621
+ ``bias.pl``). ``<filestem>.f`` and ``<filestem>.n`` are the positive
622
+ and negative halves of :meth:`aleph_examples_text`.
623
+
624
+ Same discipline as :meth:`write` throughout: the PARENT of
625
+ ``directory`` must already exist, every file is RENDERED before the
626
+ directory is created so a refused rendering leaves nothing behind,
627
+ and every file is written with ``\\n`` line endings on every
628
+ platform.
629
+
630
+ Returns:
631
+ ``{"b": path, "f": path, "n": path}``.
632
+ """
633
+ parent = os.path.dirname(os.path.abspath(directory))
634
+ if not os.path.isdir(parent):
635
+ raise IlpEncodingError(
636
+ f"IlpTask.write_aleph: the parent directory {parent!r} does "
637
+ "not exist")
638
+ rendered = (
639
+ ("b", self.background_text() + "\n" + self.aleph_bias_text()),
640
+ ("f", self.aleph_examples_text(True)),
641
+ ("n", self.aleph_examples_text(False)),
642
+ )
643
+ os.makedirs(directory, exist_ok=True)
644
+ paths = {}
645
+ for stem, text in rendered:
646
+ path = os.path.join(directory, f"{filestem}.{stem}")
647
+ with io.open(path, "w", encoding="utf-8", newline="\n") as handle:
648
+ handle.write(text)
649
+ paths[stem] = path
650
+ return paths
651
+
652
+ # -- reading the answer back --------------------------------------------
653
+
654
+ def read_clause(self, clause: str, **kwargs):
655
+ """A learned clause as a formula, in THIS task's vocabulary.
656
+
657
+ :func:`~unicode_logic_kit.ilp.clause_to_formula` with ``membership`` and
658
+ ``predicates`` filled in from the task, so the predicate names come
659
+ back exactly as they were emitted and the result can be evaluated
660
+ against the very structures the task was built from.
661
+ """
662
+ from .readback import clause_to_formula
663
+
664
+ return clause_to_formula(clause, membership=self.membership,
665
+ predicates=self.body_predicates, **kwargs)
666
+
667
+ def read_hypothesis(self, text: str, **kwargs):
668
+ """Every clause of a learner's output, in THIS task's vocabulary.
669
+
670
+ Like :meth:`read_clause`, and additionally checks that each clause
671
+ defines this task's target. The clauses come back separately — see
672
+ :func:`~unicode_logic_kit.ilp.hypothesis_to_formulas` for why they are
673
+ not disjoined for you.
674
+ """
675
+ from .readback import hypothesis_to_formulas
676
+
677
+ kwargs.setdefault("target", self.target)
678
+ return hypothesis_to_formulas(text, membership=self.membership,
679
+ predicates=self.body_predicates,
680
+ **kwargs)
681
+
682
+ # -- reporting ----------------------------------------------------------
683
+
684
+ @property
685
+ def counts(self) -> Dict[str, int]:
686
+ """Sizes worth knowing before handing the task to a learner: how many
687
+ examples of each label, and how many facts the learner will see."""
688
+ facts = 0
689
+ for example in self.examples:
690
+ facts += len(example.structure.domain) # membership
691
+ for name, arity in self.body_predicates:
692
+ if example.structure.interprets(name, arity):
693
+ facts += len(self._rows(example.structure, name, arity))
694
+ return {
695
+ "positive": sum(1 for e in self.examples if e.label),
696
+ "negative": sum(1 for e in self.examples if not e.label),
697
+ "predicates": len(self.body_predicates),
698
+ "facts": facts,
699
+ }
700
+
701
+
702
+ ExampleSpec = Union[
703
+ Mapping[str, FiniteStructure],
704
+ Sequence[Tuple[str, FiniteStructure]],
705
+ ]
706
+
707
+
708
+ def _as_examples(spec: ExampleSpec, label: bool) -> List[Example]:
709
+ items: Iterable[Tuple[str, FiniteStructure]]
710
+ items = spec.items() if isinstance(spec, Mapping) else spec
711
+ return [Example(str(key), structure, label) for key, structure in items]
712
+
713
+
714
+ def task_from_structures(target: str, positive: ExampleSpec,
715
+ negative: ExampleSpec, **kwargs) -> IlpTask:
716
+ """An :class:`IlpTask` from two id→structure collections.
717
+
718
+ A convenience over building :class:`Example` objects by hand; every
719
+ keyword argument is passed straight through to :class:`IlpTask`, and every
720
+ check happens there.
721
+
722
+ Args:
723
+ target: the predicate to learn.
724
+ positive: ``{"m1": structure, …}`` or ``[("m1", structure), …]``.
725
+ negative: the same, for the negative examples.
726
+ """
727
+ return IlpTask(target,
728
+ tuple(_as_examples(positive, True)
729
+ + _as_examples(negative, False)),
730
+ **kwargs)