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,219 @@
1
+ """A wall-clock limit shared by the kit's in-house searches.
2
+
3
+ Every in-house search (the classical and the labelled tableaux, resolution, the finite model
4
+ finder, the Kripke enumerator, the LTL tableau) takes its limit in milliseconds and turns it into
5
+ one instant on the ``time.perf_counter()`` clock when the call starts. This module holds the two
6
+ things they share.
7
+
8
+ * :class:`DeadlineReached` is what a search raises from deep inside its own loops to unwind to its
9
+ entry point, which answers "no answer within the limit". It is raised by the search itself, at
10
+ a place the search chose.
11
+
12
+ * :func:`run_until` runs a function that has no loop of its own to put a check in: it is the
13
+ normal-form conversion that precedes a resolution search, or a routine of another module that
14
+ the search only calls. It ends the call at the deadline by raising an exception in the calling
15
+ thread (``PyThreadState_SetAsyncExc``), which Python delivers at the next bytecode, so a
16
+ conversion that blows up exponentially is cut off within a few milliseconds of the deadline
17
+ and costs nothing before it. A call into C code (a long ``itertools`` step, a solver) is not
18
+ interrupted until it returns, and a function must be safe to abandon half-way: it may build
19
+ data of its own but must not leave a shared structure half-written.
20
+
21
+ **When the interruption can arrive.** An asynchronous exception is delivered at whatever
22
+ bytecode the thread executes next, so the helper decides, for every instant of a call, whether
23
+ the timer may post one and where it would land.
24
+
25
+ * Before the call arms the timer nothing can be posted: the timer thread is started in a state
26
+ in which it only notes that it ran out, and a call whose timer ran out first gives up without
27
+ running the function.
28
+ * From the moment the call arms the timer until its own clean-up has withdrawn what the timer
29
+ may have left, every bytecode of the thread lies inside the one ``try`` statement that catches
30
+ this call's own exception class: the function, the instants between its return and the
31
+ clean-up, and the clean-up itself.
32
+ * The clean-up is one critical section: it ends the timer's right to post and takes back an
33
+ exception that was posted but not yet delivered. After it, nothing of this call is pending,
34
+ and what runs next (the handler, the caller's code) is never interrupted by this call.
35
+ * The clean-up is repeated by an outer ``finally``, so another exception that cuts the first
36
+ one short (a ``KeyboardInterrupt``, the cut-off of an enclosing call) cannot leave the timer
37
+ armed.
38
+ * Calls in different threads do not meet: each call has its own timer, its own lock and its own
39
+ exception class, and the exception is posted to the thread that made the call.
40
+
41
+ Two things the helper cannot do. A thread has room for ONE pending asynchronous exception, so
42
+ of two nested limits that expire within the same few microseconds only one is delivered, and
43
+ the enclosing call may miss its own limit and run on to its end; it is never interrupted at a
44
+ place where nothing is waiting for it. And Python prints and drops an exception that is raised
45
+ inside a ``__del__`` or a weak-reference callback, so a function that happens to run one at the
46
+ very instant the exception lands is not cut off (the call then ends late, with its own answer).
47
+ """
48
+
49
+ import threading
50
+ import time
51
+ from typing import Any, Callable, Optional, Tuple, Type, TypeVar
52
+
53
+ try:
54
+ import ctypes
55
+ _SET_ASYNC_EXC: Optional[Any] = ctypes.pythonapi.PyThreadState_SetAsyncExc
56
+ except Exception: # an interpreter without ctypes, or without the C API through it
57
+ ctypes = None # type: ignore[assignment]
58
+ _SET_ASYNC_EXC = None
59
+
60
+ __all__ = ["DeadlineReached", "instant", "passed", "remaining_ms", "run_until"]
61
+
62
+ T = TypeVar("T")
63
+
64
+
65
+ class DeadlineReached(Exception):
66
+ """A search ran past its deadline (raised by the search, caught at its entry point)."""
67
+
68
+
69
+ def instant(timeout_ms: Optional[float]) -> Optional[float]:
70
+ """The ``perf_counter`` instant ``timeout_ms`` milliseconds from now; ``None`` for no limit."""
71
+ return None if timeout_ms is None else time.perf_counter() + timeout_ms / 1000.0
72
+
73
+
74
+ def passed(deadline: Optional[float]) -> bool:
75
+ """True iff ``deadline`` (an instant from :func:`instant`) is in the past; never for ``None``."""
76
+ return deadline is not None and time.perf_counter() > deadline
77
+
78
+
79
+ def remaining_ms(deadline: Optional[float]) -> Optional[float]:
80
+ """The milliseconds left until ``deadline`` (not below 0), or ``None`` for no limit."""
81
+ if deadline is None:
82
+ return None
83
+ return max(0.0, (deadline - time.perf_counter()) * 1000.0)
84
+
85
+
86
+ class _Interrupted(BaseException):
87
+ """Delivered into the calling thread at the deadline by :func:`run_until`.
88
+
89
+ A ``BaseException``, so that a broad ``except Exception`` somewhere in the interrupted code
90
+ cannot swallow it; :func:`run_until` is the only place that catches it.
91
+ """
92
+
93
+
94
+ #: Whether this interpreter can raise an exception in a running thread (CPython can).
95
+ _CAN_INTERRUPT = _SET_ASYNC_EXC is not None
96
+
97
+
98
+ def _set_async_exception(thread_id: int, exception: Optional[type]) -> int:
99
+ """Ask the interpreter to raise ``exception`` in thread ``thread_id`` (``None`` clears a pending one)."""
100
+ assert _SET_ASYNC_EXC is not None
101
+ return _SET_ASYNC_EXC(ctypes.c_ulong(thread_id), ctypes.py_object(exception) if exception else None)
102
+
103
+
104
+ #: The states of a :class:`_Cutoff`.
105
+ _IDLE = 0 # the function has not begun: a timer that runs out now only notes it
106
+ _ARMED = 1 # the function runs under the limit: a timer that runs out now posts
107
+ _POSTED = 2 # the interruption was posted: delivered by now, or still pending
108
+ _EXPIRED = 3 # the timer ran out before the function began: the function must not run
109
+ _DONE = 4 # the call is over: the timer has no right left
110
+
111
+
112
+ class _Cutoff:
113
+ """What the calling thread and the timer thread of one :func:`run_until` call share.
114
+
115
+ A state that only changes under one lock, so that the two never disagree about whether an
116
+ interruption has been posted:
117
+
118
+ * ``_IDLE`` → ``_ARMED`` when the calling thread is about to run the function (:meth:`arm`);
119
+ * ``_ARMED`` → ``_POSTED`` when the timer runs out and posts the interruption to the thread
120
+ (:meth:`fire`);
121
+ * ``_IDLE`` → ``_EXPIRED`` when the timer runs out first, which tells :meth:`arm` that the
122
+ limit is over already (nothing is posted: the thread is not inside the guarded region);
123
+ * any state → ``_DONE`` on :meth:`disarm`, which withdraws a posted interruption that was not
124
+ delivered yet.
125
+
126
+ Every call has its own exception class, so a nested call is cut off by its own timer and by
127
+ no other.
128
+
129
+ The timer itself is NOT a field. A timer that this object also pointed back to would make a
130
+ reference cycle, and a cycle's garbage is collected whenever the collector next runs: its
131
+ clean-up (a weak-reference callback of the ``threading`` module that drops the finished timer
132
+ thread) would then run in the middle of some later call's function, and an exception
133
+ delivered into such a callback is printed and dropped, which loses that call's cut-off.
134
+ Without the cycle the timer thread's object dies, by its reference count, when
135
+ :func:`run_until` returns.
136
+ """
137
+
138
+ __slots__ = ("thread_id", "interrupt", "_lock", "_state")
139
+
140
+ def __init__(self) -> None:
141
+ self.thread_id = threading.get_ident()
142
+ self.interrupt: Type[BaseException] = type("_Interrupted", (_Interrupted,), {})
143
+ self._lock = threading.Lock()
144
+ self._state = _IDLE
145
+
146
+ def fire(self) -> None:
147
+ """Run on the timer thread when the limit is over."""
148
+ with self._lock:
149
+ if self._state == _ARMED:
150
+ self._state = _POSTED
151
+ _set_async_exception(self.thread_id, self.interrupt)
152
+ elif self._state == _IDLE:
153
+ self._state = _EXPIRED
154
+
155
+ def arm(self) -> bool:
156
+ """Let the timer post from now on; False if its limit is over already (run nothing)."""
157
+ with self._lock:
158
+ if self._state != _IDLE:
159
+ return False
160
+ self._state = _ARMED
161
+ return True
162
+
163
+ def disarm(self) -> None:
164
+ """End the timer's right to post, and take back an interruption it posted and left pending.
165
+
166
+ Idempotent, so that a second call can complete a first one that another exception cut
167
+ short: a state that is not ``_DONE`` yet says what is left to do.
168
+ """
169
+ with self._lock: # a timer that is posting has finished by now
170
+ if self._state == _POSTED:
171
+ _set_async_exception(self.thread_id, None)
172
+ self._state = _DONE
173
+
174
+
175
+ def run_until(deadline: Optional[float], function: Callable[[], T]) -> Tuple[bool, Optional[T]]:
176
+ """Run ``function()``; return ``(True, value)``, or ``(False, None)`` if it was cut off at ``deadline``.
177
+
178
+ ``deadline`` is an instant from :func:`instant`; ``None`` means no limit, and then this is a
179
+ plain call. A deadline that has passed already runs nothing, and so does one that passes
180
+ while the call is being set up (the answer is ``(False, None)`` either way). When the
181
+ deadline passes while ``function`` is still running, an exception is raised in this thread
182
+ at the next bytecode and ends it: the result is ``(False, None)`` and nothing the function
183
+ built is returned. An interruption that arrives after the function returned, or inside the
184
+ clean-up of this call, is absorbed: the result is then ``(False, None)`` if it cut the
185
+ return short and ``(True, value)`` if the function had finished. Nothing of this call is
186
+ ever raised into the caller, whatever the limit and however many threads call at once. A
187
+ function that finishes first costs one timer thread and nothing else. Nested calls are
188
+ independent: each cuts off only its own function (see the module docstring for the one
189
+ case in which an enclosing call can be late). On an interpreter that cannot raise an
190
+ exception in a running thread the function simply runs to its end.
191
+
192
+ ``function`` must be safe to abandon half-way (see the module docstring), and must not catch
193
+ ``BaseException``. An exception that ``function`` raises on its own propagates unchanged.
194
+ """
195
+ if deadline is None or not _CAN_INTERRUPT:
196
+ return True, function()
197
+ if passed(deadline):
198
+ return False, None
199
+ cutoff = _Cutoff()
200
+ timer = threading.Timer(max(0.0, deadline - time.perf_counter()), cutoff.fire)
201
+ timer.daemon = True
202
+ value: Optional[T] = None
203
+ finished = False
204
+ try:
205
+ try:
206
+ try:
207
+ timer.start() # cannot post yet: the call is not armed
208
+ if cutoff.arm():
209
+ value = function()
210
+ finished = True
211
+ finally:
212
+ cutoff.disarm()
213
+ timer.cancel()
214
+ except cutoff.interrupt:
215
+ pass
216
+ finally:
217
+ cutoff.disarm() # again: finishes a clean-up that was cut short
218
+ timer.cancel()
219
+ return finished, (value if finished else None)
@@ -0,0 +1,126 @@
1
+ """The ``ace`` subpackage — Attempto Controlled English, via APE.
2
+
3
+ ACE is a controlled natural language with exactly ONE reading per sentence,
4
+ fixed by documented interpretation rules rather than by a statistical guesser
5
+ — which makes it the natural English-shaped INPUT format for a kit whose other
6
+ readers (TPTP, Prover9, SMT-LIB, SBN, CASL, …) already assume that a text
7
+ means one thing. The reference parser APE is driven as an external subprocess
8
+ (LGPL; never vendored, never reimplemented — see
9
+ :mod:`unicode_logic_kit.ace.runner`'s module docstring for why a partial APE
10
+ clone would be worse than none).
11
+
12
+ What works today (milestone ACE-1) is the TPTP route: ACE text → APE → TPTP →
13
+ :func:`unicode_logic_kit.fol.tptp_input.parse_tptp` → kit formulas, plus a
14
+ mechanical per-sentence coverage report. Three outcomes, none silent:
15
+
16
+ >>> from unicode_logic_kit.ace import ace_to_fol # doctest: +SKIP
17
+ >>> [f.to_unicode_str() for f in ace_to_fol("Every man waits.")] # doctest: +SKIP
18
+ ['∀a (Man(a) → ∃b Predicate1(b, wait, a))']
19
+
20
+ - a sentence that is not ACE raises :class:`~unicode_logic_kit.ace.runner.AceParseError`
21
+ with APE's own diagnosis (position, token, repair hint);
22
+ - an ACE sentence whose DRS uses modality, negation as failure, a question or
23
+ a command raises :class:`~unicode_logic_kit.ace.runner.AceTptpUnsupportedError`
24
+ — Attempto's OWN TPTP translator refuses those, and the kit routes them
25
+ into its modal family in milestone ACE-3 instead of mistranslating today;
26
+ - a plural cardinality ("at least 3 men") survives only REIFIED on THIS
27
+ route (Attempto's export kept verbatim) and is flagged by
28
+ :func:`~unicode_logic_kit.ace.runner.ace_coverage` (``reified_cardinality``)
29
+ — the DRS and formula routes below carry the counting force (ACE-4/5).
30
+
31
+ Since milestone ACE-2/3 the DRS itself is first-class, and three routes share
32
+ one vocabulary (pinned against each other by a Z3 differential over the whole
33
+ corpus — see ``tests/test_ace_mapping.py``):
34
+
35
+ - :func:`~unicode_logic_kit.ace.runner.ace_to_fol` — Attempto's own TPTP,
36
+ through the kit's reader (fast, but only what THEIR translator covers);
37
+ - :func:`~unicode_logic_kit.ace.mapping.ace_to_drs` — APE's DRS read 1:1
38
+ (:func:`~unicode_logic_kit.ace.drs_reader.parse_ape_drs`) and mapped onto
39
+ :mod:`unicode_logic_kit.drt`'s core, every condition reported
40
+ (:class:`~unicode_logic_kit.ace.mapping.DrsMapping`), nothing dropped
41
+ silently — since ACE-5 including plurals and cardinalities, collectively,
42
+ on the ``Card``/``Part`` conditions;
43
+ - :func:`~unicode_logic_kit.ace.translate.ace_to_formula` — straight to one
44
+ kit formula, INCLUDING what a DRS cannot hold: ACE's four modal boxes
45
+ become □/◇/Ⓞ/Ⓟ, a wh-question an open formula, a yes/no question a
46
+ closed one with its interrogative force kept on ``kind``, the
47
+ ``exactly``/``at most`` maximality a counting quantifier, and arithmetic
48
+ kit terms (decided by :func:`~unicode_logic_kit.atp.z3_arith.is_valid_arith`).
49
+
50
+ Still refused everywhere, by name: commands and negation as failure (both
51
+ honestly undecided — no milestone).
52
+
53
+ Since ACE-6 the pipeline also runs BACKWARDS:
54
+ :func:`~unicode_logic_kit.ace.verbalize.drs_to_ace` verbalizes a kit DRS as
55
+ ACE text plus the user-lexicon entries that carry its content words, and
56
+ :func:`~unicode_logic_kit.ace.verbalize.ace_round_trip` is the machine
57
+ self-check — text through APE and back, judged by Z3 (the whole mappable
58
+ corpus closes the loop). :func:`~unicode_logic_kit.ace.chem_lexicon.chem_ulex`
59
+ renders the ChemLog signature as such a lexicon, so ACE sentences can talk
60
+ about molecules in plain words ("a carbon", "bonds", "aromatic") while the
61
+ DRS underneath carries the declared chemistry vocabulary.
62
+
63
+ Since ACE-7 the backward direction also covers the modal/deontic fragment
64
+ (question/command generation stays deferred):
65
+ :func:`~unicode_logic_kit.ace.reverse_modal.fol_to_modal_drs` recognizes the
66
+ two shapes ACE's modal surface can carry — a modality wrapping a whole
67
+ formula ("John must wait.") and one nested in a duplex's consequent ("Every
68
+ man must wait.", verbalized as "If there is a man X1 then X1 must wait.") —
69
+ as :class:`~unicode_logic_kit.ace.reverse_modal.ModalBox` /
70
+ :class:`~unicode_logic_kit.ace.reverse_modal.ModalImpl`, reusing
71
+ :func:`~unicode_logic_kit.drt.reverse.fol_to_drs` for every classical piece
72
+ and refusing every other placement by the same message that route already
73
+ gives a bare modal node. :func:`~unicode_logic_kit.ace.verbalize.modal_formula_to_ace`
74
+ is the ACE-6 entry point's modal counterpart, and
75
+ :func:`~unicode_logic_kit.ace.verbalize.modal_ace_round_trip` closes the loop
76
+ through APE, judged by :func:`~unicode_logic_kit.eval.equivalence.equivalent`
77
+ (not raw Z3 — a modal ``Node`` has no direct Z3 export; ``equivalent``
78
+ already routes a modal pair through the modal tableau / QML embedding).
79
+ """
80
+
81
+ from .drs_reader import (
82
+ AceAtom, AceCommand, AceCondList, AceDrs, AceDrsUnreadError, AceExpr,
83
+ AceImpl, AceInt, AceModal, AceNaf, AceNamed, AceNeg, AceOr, AceQuestion,
84
+ AceReal, AceString, AceTermApp, AceVar, parse_ape_drs,
85
+ )
86
+ from .mapping import (
87
+ AceUnsupportedError, ConditionReport, DrsMapping, ace_to_drs,
88
+ condition_statistics, map_ace_drs,
89
+ )
90
+ from .runner import (
91
+ AceError, AceParseError, AceTptpUnreadError, AceTptpUnsupportedError,
92
+ ApeMessage, ApeResult, ApeUnavailableError, CoverageRow, ace_coverage,
93
+ ace_to_fol, ape_available, run_ape,
94
+ )
95
+ from .translate import AceFormula, ace_drs_to_formula, ace_to_formula
96
+ from .reverse_modal import ModalBox, ModalImpl, fol_to_modal_drs
97
+ from .verbalize import (
98
+ AceRoundTrip, AceText, AceVerbalizationError, ModalAceRoundTrip,
99
+ ace_round_trip, drs_to_ace, formula_to_ace, modal_ace_round_trip,
100
+ modal_drs_to_ace, modal_formula_to_ace,
101
+ )
102
+ from .chem_lexicon import ace_kit_name, chem_ulex
103
+
104
+ __all__ = [
105
+ # the engine and the three routes
106
+ "ape_available", "run_ape", "ace_to_fol", "ace_to_drs", "ace_to_formula",
107
+ "ace_coverage", "map_ace_drs", "ace_drs_to_formula", "parse_ape_drs",
108
+ "condition_statistics",
109
+ # the reverse direction (ACE-6)
110
+ "drs_to_ace", "formula_to_ace", "ace_round_trip", "chem_ulex",
111
+ "ace_kit_name",
112
+ # the reverse direction, modal/deontic fragment (ACE-7)
113
+ "fol_to_modal_drs", "ModalBox", "ModalImpl", "modal_drs_to_ace",
114
+ "modal_formula_to_ace", "modal_ace_round_trip", "ModalAceRoundTrip",
115
+ # results and reports
116
+ "ApeResult", "ApeMessage", "CoverageRow", "DrsMapping",
117
+ "ConditionReport", "AceFormula", "AceText", "AceRoundTrip",
118
+ # the 1:1 DRS model
119
+ "AceDrs", "AceVar", "AceNamed", "AceInt", "AceReal", "AceString",
120
+ "AceExpr", "AceTermApp", "AceAtom", "AceNeg", "AceNaf", "AceImpl",
121
+ "AceOr", "AceModal", "AceQuestion", "AceCommand", "AceCondList",
122
+ # errors
123
+ "AceError", "ApeUnavailableError", "AceParseError",
124
+ "AceTptpUnsupportedError", "AceTptpUnreadError", "AceDrsUnreadError",
125
+ "AceUnsupportedError", "AceVerbalizationError",
126
+ ]
@@ -0,0 +1,135 @@
1
+ """Align APE's reified TPTP vocabulary onto the mapping's — differential glue.
2
+
3
+ The ACE-2 correctness argument is a differential: for every corpus sentence
4
+ that BOTH routes cover, the kit-DRS route's FOL
5
+ (:func:`unicode_logic_kit.drt.export.drs_to_fol` over
6
+ :func:`unicode_logic_kit.ace.mapping.map_ace_drs`) must be Z3-equivalent to
7
+ Attempto's own reference translation (the TPTP output, read by
8
+ :func:`unicode_logic_kit.fol.tptp_input.parse_tptp`). The two sides speak
9
+ different dialects of the same signature — APE reifies what the mapping
10
+ names::
11
+
12
+ Predicate2(e, see, s, o) vs. See(e, s, o)
13
+ Property1(a, rich, pos) vs. Rich(a)
14
+ Property2(a, tall, comp_than, m) vs. Tall_comp_than(a, m)
15
+ Relation(a, of, b) vs. Of(a, b)
16
+ Modifier_adv(e, loudly, pos) vs. Loudly(e)
17
+ Object(a, water, mass, na, na, na) vs. Water(a)
18
+ 'John', 30, string('Johnny') vs. john, c_30, c_Johnny
19
+
20
+ This module rewrites the APE side into the mapping side, atom by atom, with
21
+ the SAME name rules the mapping uses (imported from it, not re-implemented),
22
+ so the differential compares semantics rather than spelling. It is private
23
+ test machinery in package form: the rewrite rules are themselves a claim
24
+ about the correspondence, and the differential is what checks that claim —
25
+ a wrong rule here makes sentences INequivalent and the test loud, never the
26
+ other way around. Anything outside the measured inventory raises.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from typing import List
32
+
33
+ from ..fol.nodes import (
34
+ And, Atom, Constant, Function, Implies, Node, Not, Number, Or, Quantifier,
35
+ Variable,
36
+ )
37
+ from .mapping import _degree_name, _kit_predicate, _named_constant
38
+
39
+ __all__ = ["align_ape_tptp_formula", "conjoin"]
40
+
41
+
42
+ def conjoin(formulas: List[Node]) -> Node:
43
+ """One formula per sentence → one formula per text (∧-fold)."""
44
+ if not formulas:
45
+ raise ValueError("conjoin: no formulas")
46
+ result = formulas[0]
47
+ for f in formulas[1:]:
48
+ result = And(result, f)
49
+ return result
50
+
51
+
52
+ def align_ape_tptp_formula(formula: Node) -> Node:
53
+ """Rewrite one parsed APE-TPTP formula into the mapping vocabulary."""
54
+ return _align(formula)
55
+
56
+
57
+ def _align(node: Node) -> Node:
58
+ if isinstance(node, Quantifier):
59
+ return Quantifier(node.type, node.variable, _align(node.formula))
60
+ if isinstance(node, And):
61
+ return And(_align(node.left), _align(node.right))
62
+ if isinstance(node, Or):
63
+ return Or(_align(node.left), _align(node.right))
64
+ if isinstance(node, Implies):
65
+ return Implies(_align(node.left), _align(node.right))
66
+ if isinstance(node, Not):
67
+ return Not(_align(node.formula))
68
+ if isinstance(node, Atom):
69
+ return _align_atom(node)
70
+ raise NotImplementedError(
71
+ f"APE TPTP alignment: {type(node).__name__} is not in the measured "
72
+ "inventory of APE outputs")
73
+
74
+
75
+ def _atom_name(term: Node, what: str) -> str:
76
+ if isinstance(term, Constant):
77
+ return term.name
78
+ raise NotImplementedError(
79
+ f"APE TPTP alignment: expected a constant {what}, got "
80
+ f"{type(term).__name__}")
81
+
82
+
83
+ def _align_atom(atom: Atom) -> Atom:
84
+ p, args = atom.predicate, atom.args
85
+ if p in ("Predicate1", "Predicate2", "Predicate3") and len(args) >= 3:
86
+ verb = _atom_name(args[1], "verb")
87
+ participants = tuple(_align_term(a) for a in (args[0],) + args[2:])
88
+ return Atom(_kit_predicate(verb), participants)
89
+ if p in ("Property1", "Property2") and len(args) >= 3:
90
+ adjective = _atom_name(args[1], "adjective")
91
+ degree = _atom_name(args[2], "degree")
92
+ rest = tuple(_align_term(a) for a in (args[0],) + args[3:])
93
+ return Atom(_degree_name(adjective, degree), rest)
94
+ if p == "Relation" and len(args) == 3:
95
+ middle = _atom_name(args[1], "relation kind")
96
+ if middle != "of":
97
+ raise NotImplementedError(
98
+ f"APE TPTP alignment: relation kind {middle!r} not measured")
99
+ return Atom("Of", (_align_term(args[0]), _align_term(args[2])))
100
+ if p == "Modifier_adv" and len(args) == 3:
101
+ adverb = _atom_name(args[1], "adverb")
102
+ degree = _atom_name(args[2], "degree")
103
+ return Atom(_degree_name(adverb, degree), (_align_term(args[0]),))
104
+ if p == "Modifier_pp" and len(args) == 3:
105
+ preposition = _atom_name(args[1], "preposition")
106
+ return Atom(_kit_predicate(preposition),
107
+ (_align_term(args[0]), _align_term(args[2])))
108
+ if p == "Object" and len(args) == 6:
109
+ noun = _atom_name(args[1], "noun")
110
+ return Atom(_kit_predicate(noun), (_align_term(args[0]),))
111
+ # Everything else — the prettified unary nouns (Man, Dog), equality —
112
+ # already speaks the mapping vocabulary up to its TERMS.
113
+ return Atom(p, tuple(_align_term(a) for a in args))
114
+
115
+
116
+ def _align_term(term: Node) -> Node:
117
+ if isinstance(term, Variable):
118
+ return term
119
+ if isinstance(term, Constant):
120
+ return Constant(_named_constant(term.name))
121
+ if isinstance(term, Number):
122
+ value = term.value
123
+ if isinstance(value, int) or value == int(value):
124
+ text = str(int(value)) if value >= 0 else "m" + str(-int(value))
125
+ else:
126
+ text = ("m" if value < 0 else "") + str(abs(value)).replace(".", "p")
127
+ return Constant("c_" + text)
128
+ if isinstance(term, Function) and term.name == "string" and len(term.args) == 1:
129
+ inner = term.args[0]
130
+ if isinstance(inner, Constant):
131
+ cleaned = "".join(ch for ch in inner.name if ch.isalnum())
132
+ return Constant("c_" + (cleaned if cleaned else "str"))
133
+ raise NotImplementedError(
134
+ f"APE TPTP alignment: term {type(term).__name__} is not in the "
135
+ "measured inventory")
@@ -0,0 +1,128 @@
1
+ """The ChemLog vocabulary as an APE user lexicon — ACE about molecules.
2
+
3
+ :func:`chem_ulex` renders every speakable predicate of
4
+ :data:`unicode_logic_kit.chem.signature.CHEMLOG_SIGNATURE` as APE
5
+ ``-ulextext`` entries, so ACE sentences can talk about molecules in plain
6
+ words while the DRS underneath carries the ChemLog symbols::
7
+
8
+ from unicode_logic_kit.ace import ace_to_drs, chem_ulex
9
+ drs = ace_to_drs("There is a carbon X1. There is an oxygen X2. "
10
+ "X1 bonds X2. X1 is aromatic.", ulex=chem_ulex())
11
+
12
+ The surface/symbol split is the whole point: the ENGLISH side says
13
+ "carbon", "bonds", "aromatic"; the LOGICAL side of each lexicon entry is
14
+ the ChemLog predicate itself (``c``, ``bond``, ``aromatic``), so what
15
+ arrives in the DRS is the declared chemistry vocabulary, not a parallel
16
+ spelling. Two shape differences against the raw signature are inherent to
17
+ ACE and documented rather than papered over:
18
+
19
+ - **Capitalization**: the kit's DRS/formula routes spell predicates in the
20
+ kit convention (``c`` → ``C``, ``bond`` → ``Bond``, ``bSINGLE`` →
21
+ ``BSINGLE``) — :func:`ace_kit_name` computes the arriving spelling for
22
+ any ChemLog predicate.
23
+ - **Events**: an ACE verb always carries a Davidsonian event, so the
24
+ binary ChemLog relations arrive with arity THREE (``Bond(e1, x1, x2)``).
25
+ Projecting the event away (``∃e Bond(e, x, y)`` → ``bond(x, y)``) is a
26
+ deliberate, separate step for the caller — this module does not do it
27
+ silently.
28
+
29
+ The three NULLARY predicates (``net_charge_neutral``,
30
+ ``NetChargePositive``, ``NetChargeNegative``) are unspeakable in ACE —
31
+ a sentence needs a subject — and are excluded by name
32
+ (:data:`CHEM_UNSPEAKABLE`); the coverage test pins that the three tables
33
+ plus this exclusion list tile the signature exactly, so a signature change
34
+ lands here loudly.
35
+
36
+ Category choices: elements and ``atom`` are NOUNS ("a carbon"), atom
37
+ properties are ADJECTIVES ("X1 is aromatic"), binary relations are
38
+ TRANSITIVE VERBS ("X1 bonds X2"). Hyphenated surfaces are ordinary ACE
39
+ words (probed: quoted-atom lexicon entries parse fine).
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ from typing import Dict, Tuple
45
+
46
+ from .mapping import _kit_predicate
47
+ from .verbalize import _atom, _s_form
48
+
49
+ __all__ = ["chem_ulex", "ace_kit_name", "CHEM_NOUNS", "CHEM_ADJECTIVES",
50
+ "CHEM_VERBS", "CHEM_UNSPEAKABLE"]
51
+
52
+
53
+ #: ChemLog predicate → ACE noun (singular surface; the plural adds ``s``).
54
+ CHEM_NOUNS: Dict[str, str] = {
55
+ "at": "astatine",
56
+ "atom": "atom",
57
+ "br": "bromine",
58
+ "c": "carbon",
59
+ "cl": "chlorine",
60
+ "f": "fluorine",
61
+ "h": "hydrogen",
62
+ "i": "iodine",
63
+ "n": "nitrogen",
64
+ "o": "oxygen",
65
+ "p": "phosphorus",
66
+ "s": "sulfur",
67
+ }
68
+
69
+ #: ChemLog predicate → ACE adjective ("X1 is aromatic.").
70
+ CHEM_ADJECTIVES: Dict[str, str] = {
71
+ "ChiralR": "r-chiral",
72
+ "ChiralS": "s-chiral",
73
+ "aromatic": "aromatic",
74
+ "charge0": "uncharged",
75
+ "charge_m1": "negatively-charged",
76
+ "charge_p1": "positively-charged",
77
+ "has_0_hs": "zero-hydrogen",
78
+ "has_1_hs": "one-hydrogen",
79
+ "has_2_hs": "two-hydrogen",
80
+ "has_3_hs": "three-hydrogen",
81
+ "in_ring": "cyclic",
82
+ "in_ring_of_size_3": "three-membered",
83
+ "in_ring_of_size_4": "four-membered",
84
+ "in_ring_of_size_5": "five-membered",
85
+ "in_ring_of_size_6": "six-membered",
86
+ "in_ring_of_size_7": "seven-membered",
87
+ "in_ring_of_size_8": "eight-membered",
88
+ }
89
+
90
+ #: ChemLog predicate → (finite-singular, infinitive/plural) verb surfaces.
91
+ CHEM_VERBS: Dict[str, Tuple[str, str]] = {
92
+ "bAROMATIC": ("aromatically-bonds", "aromatically-bond"),
93
+ "bDOUBLE": ("double-bonds", "double-bond"),
94
+ "bSINGLE": ("single-bonds", "single-bond"),
95
+ "bTRIPLE": ("triple-bonds", "triple-bond"),
96
+ "bond": ("bonds", "bond"),
97
+ "carbon_connected": ("carbon-connects-to", "carbon-connect-to"),
98
+ "has_bond_to": ("has-a-bond-to", "have-a-bond-to"),
99
+ "same_fragment": ("shares-a-fragment-with", "share-a-fragment-with"),
100
+ }
101
+
102
+ #: The nullary ChemLog predicates — no ACE sentence can state a
103
+ #: subject-less proposition, so these have no surface, by name.
104
+ CHEM_UNSPEAKABLE = frozenset({
105
+ "NetChargeNegative", "NetChargePositive", "net_charge_neutral",
106
+ })
107
+
108
+
109
+ def chem_ulex() -> str:
110
+ """The whole speakable ChemLog vocabulary as ``-ulextext`` clauses."""
111
+ lines = []
112
+ for logical, noun in CHEM_NOUNS.items():
113
+ lines.append(f"noun_sg({_atom(noun)}, {_atom(logical)}, neutr).")
114
+ lines.append(
115
+ f"noun_pl({_atom(_s_form(noun))}, {_atom(logical)}, neutr).")
116
+ for logical, adj in CHEM_ADJECTIVES.items():
117
+ lines.append(f"adj_itr({_atom(adj)}, {_atom(logical)}).")
118
+ for logical, (finsg, infpl) in CHEM_VERBS.items():
119
+ lines.append(f"tv_finsg({_atom(finsg)}, {_atom(logical)}).")
120
+ lines.append(f"tv_infpl({_atom(infpl)}, {_atom(logical)}).")
121
+ return "\n".join(sorted(lines))
122
+
123
+
124
+ def ace_kit_name(chemlog_name: str) -> str:
125
+ """The kit-convention spelling a ChemLog predicate arrives under when
126
+ an ACE sentence travels the DRS or formula route (``c`` → ``C``,
127
+ ``bond`` → ``Bond``, ``bSINGLE`` → ``BSINGLE``)."""
128
+ return _kit_predicate(chemlog_name)