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.
- unicode_logic_kit/__init__.py +385 -0
- unicode_logic_kit/__main__.py +520 -0
- unicode_logic_kit/_deadline.py +219 -0
- unicode_logic_kit/ace/__init__.py +126 -0
- unicode_logic_kit/ace/_align.py +135 -0
- unicode_logic_kit/ace/chem_lexicon.py +128 -0
- unicode_logic_kit/ace/drs_reader.py +570 -0
- unicode_logic_kit/ace/mapping.py +666 -0
- unicode_logic_kit/ace/reverse_modal.py +138 -0
- unicode_logic_kit/ace/runner.py +551 -0
- unicode_logic_kit/ace/translate.py +452 -0
- unicode_logic_kit/ace/verbalize.py +1070 -0
- unicode_logic_kit/api.py +1284 -0
- unicode_logic_kit/atp/__init__.py +177 -0
- unicode_logic_kit/atp/_ascii_names.py +113 -0
- unicode_logic_kit/atp/_html.py +72 -0
- unicode_logic_kit/atp/_substructural_input.py +228 -0
- unicode_logic_kit/atp/_tff_problem.py +715 -0
- unicode_logic_kit/atp/_tptp_problem.py +1111 -0
- unicode_logic_kit/atp/_writer_support.py +289 -0
- unicode_logic_kit/atp/clingo_backend.py +1180 -0
- unicode_logic_kit/atp/cvc5_backend.py +1385 -0
- unicode_logic_kit/atp/eprover_backend.py +732 -0
- unicode_logic_kit/atp/finite_domain.py +1055 -0
- unicode_logic_kit/atp/fitch.py +1547 -0
- unicode_logic_kit/atp/fitch_search.py +551 -0
- unicode_logic_kit/atp/hets_backend.py +339 -0
- unicode_logic_kit/atp/hybrid_down.py +120 -0
- unicode_logic_kit/atp/incremental.py +250 -0
- unicode_logic_kit/atp/kripke_enum.py +741 -0
- unicode_logic_kit/atp/lambek.py +436 -0
- unicode_logic_kit/atp/leo3_backend.py +332 -0
- unicode_logic_kit/atp/linear.py +738 -0
- unicode_logic_kit/atp/lj.py +705 -0
- unicode_logic_kit/atp/logic_backends.py +566 -0
- unicode_logic_kit/atp/ltl_tableau.py +1084 -0
- unicode_logic_kit/atp/minizinc_backend.py +1402 -0
- unicode_logic_kit/atp/modal_tableau.py +1382 -0
- unicode_logic_kit/atp/nanocop_backend.py +410 -0
- unicode_logic_kit/atp/portfolio.py +489 -0
- unicode_logic_kit/atp/protocol.py +1803 -0
- unicode_logic_kit/atp/prover9_entailment.py +1153 -0
- unicode_logic_kit/atp/resolution.py +1376 -0
- unicode_logic_kit/atp/resolution_check.py +1114 -0
- unicode_logic_kit/atp/sequent.py +1050 -0
- unicode_logic_kit/atp/tableau.py +921 -0
- unicode_logic_kit/atp/tableau_check.py +543 -0
- unicode_logic_kit/atp/tptp_ncl.py +811 -0
- unicode_logic_kit/atp/tptp_tff.py +1546 -0
- unicode_logic_kit/atp/tstp.py +1333 -0
- unicode_logic_kit/atp/tstp_check.py +1096 -0
- unicode_logic_kit/atp/twee_backend.py +236 -0
- unicode_logic_kit/atp/twee_check.py +711 -0
- unicode_logic_kit/atp/twee_entailment.py +953 -0
- unicode_logic_kit/atp/vampire_entailment.py +540 -0
- unicode_logic_kit/atp/z3_arith.py +470 -0
- unicode_logic_kit/atp/z3_equivalence.py +36 -0
- unicode_logic_kit/atp/z3_fuzzy.py +362 -0
- unicode_logic_kit/atp/z3_input.py +500 -0
- unicode_logic_kit/atp/z3_models.py +208 -0
- unicode_logic_kit/chem/__init__.py +88 -0
- unicode_logic_kit/chem/_naming.py +284 -0
- unicode_logic_kit/chem/cache.py +185 -0
- unicode_logic_kit/chem/interop.py +244 -0
- unicode_logic_kit/chem/mol.py +525 -0
- unicode_logic_kit/chem/signature.py +112 -0
- unicode_logic_kit/comorphism.py +497 -0
- unicode_logic_kit/dl/__init__.py +384 -0
- unicode_logic_kit/dl/classification.py +227 -0
- unicode_logic_kit/dl/concepts.py +632 -0
- unicode_logic_kit/dl/datatypes.py +818 -0
- unicode_logic_kit/dl/owl_functional.py +2433 -0
- unicode_logic_kit/dl/owl_manchester.py +1637 -0
- unicode_logic_kit/dl/owl_reasoner.py +790 -0
- unicode_logic_kit/dl/parser.py +391 -0
- unicode_logic_kit/dl/tableau.py +4048 -0
- unicode_logic_kit/dl/translate.py +2704 -0
- unicode_logic_kit/drt/__init__.py +94 -0
- unicode_logic_kit/drt/export.py +179 -0
- unicode_logic_kit/drt/nodes.py +506 -0
- unicode_logic_kit/drt/parser.py +965 -0
- unicode_logic_kit/drt/resolve.py +195 -0
- unicode_logic_kit/drt/reverse.py +175 -0
- unicode_logic_kit/eval/__init__.py +106 -0
- unicode_logic_kit/eval/batch.py +382 -0
- unicode_logic_kit/eval/canonical.py +663 -0
- unicode_logic_kit/eval/chem_batch.py +606 -0
- unicode_logic_kit/eval/converses.py +200 -0
- unicode_logic_kit/eval/datasets/__init__.py +136 -0
- unicode_logic_kit/eval/datasets/_base.py +263 -0
- unicode_logic_kit/eval/datasets/_proofwriter_proof.py +422 -0
- unicode_logic_kit/eval/datasets/c3po.py +678 -0
- unicode_logic_kit/eval/datasets/folio.py +158 -0
- unicode_logic_kit/eval/datasets/fracas.py +418 -0
- unicode_logic_kit/eval/datasets/groves.py +191 -0
- unicode_logic_kit/eval/datasets/logicbench.py +467 -0
- unicode_logic_kit/eval/datasets/logicnli.py +303 -0
- unicode_logic_kit/eval/datasets/malls.py +133 -0
- unicode_logic_kit/eval/datasets/pfolio.py +594 -0
- unicode_logic_kit/eval/datasets/pmb.py +242 -0
- unicode_logic_kit/eval/datasets/prontoqa.py +611 -0
- unicode_logic_kit/eval/datasets/proofwriter.py +1431 -0
- unicode_logic_kit/eval/datasets/proverqa.py +674 -0
- unicode_logic_kit/eval/datasets/willow.py +478 -0
- unicode_logic_kit/eval/equivalence.py +466 -0
- unicode_logic_kit/eval/exercise_gen.py +533 -0
- unicode_logic_kit/eval/explain.py +791 -0
- unicode_logic_kit/eval/generality.py +750 -0
- unicode_logic_kit/eval/metric_hf.py +458 -0
- unicode_logic_kit/eval/predicate_match.py +343 -0
- unicode_logic_kit/eval/theory_check.py +1170 -0
- unicode_logic_kit/eval/validate.py +306 -0
- unicode_logic_kit/fol/__init__.py +177 -0
- unicode_logic_kit/fol/_atom_keys.py +510 -0
- unicode_logic_kit/fol/_fol_nodes.py +3586 -0
- unicode_logic_kit/fol/_free_parameters.py +105 -0
- unicode_logic_kit/fol/_ho_nodes.py +448 -0
- unicode_logic_kit/fol/_hybrid_nodes.py +308 -0
- unicode_logic_kit/fol/_identifiers.py +1091 -0
- unicode_logic_kit/fol/_lambek_nodes.py +112 -0
- unicode_logic_kit/fol/_linear_nodes.py +352 -0
- unicode_logic_kit/fol/_modal_nodes.py +1467 -0
- unicode_logic_kit/fol/_msfl_nodes.py +2196 -0
- unicode_logic_kit/fol/_numeral_symbols.py +231 -0
- unicode_logic_kit/fol/_so_nodes.py +200 -0
- unicode_logic_kit/fol/_symbol_names.py +81 -0
- unicode_logic_kit/fol/_team_nodes.py +181 -0
- unicode_logic_kit/fol/_tptp_symbols.py +551 -0
- unicode_logic_kit/fol/_truth_constants.py +117 -0
- unicode_logic_kit/fol/casl_export.py +1135 -0
- unicode_logic_kit/fol/casl_import.py +929 -0
- unicode_logic_kit/fol/derivation.py +367 -0
- unicode_logic_kit/fol/dialect_detect.py +70 -0
- unicode_logic_kit/fol/dialect_repair.py +537 -0
- unicode_logic_kit/fol/frames.py +637 -0
- unicode_logic_kit/fol/grammars/terminals.lark +31 -0
- unicode_logic_kit/fol/lambda_tools.py +297 -0
- unicode_logic_kit/fol/latex_input.py +429 -0
- unicode_logic_kit/fol/modal_translation.py +944 -0
- unicode_logic_kit/fol/msflparser.py +1033 -0
- unicode_logic_kit/fol/naming.py +422 -0
- unicode_logic_kit/fol/nodes.py +241 -0
- unicode_logic_kit/fol/normalforms.py +492 -0
- unicode_logic_kit/fol/pal.py +287 -0
- unicode_logic_kit/fol/prolog_export.py +566 -0
- unicode_logic_kit/fol/prolog_input.py +505 -0
- unicode_logic_kit/fol/prover9_input.py +1325 -0
- unicode_logic_kit/fol/qml.py +1760 -0
- unicode_logic_kit/fol/qmltp_input.py +525 -0
- unicode_logic_kit/fol/sanitize.py +221 -0
- unicode_logic_kit/fol/serialize.py +79 -0
- unicode_logic_kit/fol/signature.py +1290 -0
- unicode_logic_kit/fol/simplify_check.py +544 -0
- unicode_logic_kit/fol/spans.py +594 -0
- unicode_logic_kit/fol/tptp_input.py +1503 -0
- unicode_logic_kit/fol/tptp_repair.py +941 -0
- unicode_logic_kit/fol/unification.py +157 -0
- unicode_logic_kit/fol/verbalize.py +263 -0
- unicode_logic_kit/hets/__init__.py +163 -0
- unicode_logic_kit/hets/bridge.py +142 -0
- unicode_logic_kit/hets/client.py +748 -0
- unicode_logic_kit/hets/docker.py +420 -0
- unicode_logic_kit/hets/dol.py +712 -0
- unicode_logic_kit/hets/haskell_json.py +355 -0
- unicode_logic_kit/hets/owl_backend.py +794 -0
- unicode_logic_kit/hets/owl_cli.py +598 -0
- unicode_logic_kit/hets/symbols.py +512 -0
- unicode_logic_kit/hol/__init__.py +140 -0
- unicode_logic_kit/hol/_ho_common.py +323 -0
- unicode_logic_kit/hol/_isabelle_binders.py +125 -0
- unicode_logic_kit/hol/classical.py +812 -0
- unicode_logic_kit/hol/deepshallow/__init__.py +45 -0
- unicode_logic_kit/hol/deepshallow/_common.py +177 -0
- unicode_logic_kit/hol/deepshallow/conditional.py +225 -0
- unicode_logic_kit/hol/deepshallow/intuitionistic.py +181 -0
- unicode_logic_kit/hol/deepshallow/modal.py +217 -0
- unicode_logic_kit/hol/deepshallow/qml.py +406 -0
- unicode_logic_kit/hol/deepshallow/relevant.py +206 -0
- unicode_logic_kit/hol/free.py +753 -0
- unicode_logic_kit/hol/goedel.py +336 -0
- unicode_logic_kit/hol/ho_modal.py +1743 -0
- unicode_logic_kit/hol/intuitionistic.py +403 -0
- unicode_logic_kit/hol/isabelle_conditional.py +593 -0
- unicode_logic_kit/hol/isabelle_modal.py +1908 -0
- unicode_logic_kit/hol/isabelle_relevant.py +412 -0
- unicode_logic_kit/hol/isabelle_runner.py +1147 -0
- unicode_logic_kit/hol/isabelle_substructural.py +884 -0
- unicode_logic_kit/hol/lean.py +1018 -0
- unicode_logic_kit/hol/manyvalued.py +921 -0
- unicode_logic_kit/hol/secondorder.py +687 -0
- unicode_logic_kit/hol/thf_modal.py +941 -0
- unicode_logic_kit/hol/thirdorder.py +397 -0
- unicode_logic_kit/ilp/__init__.py +89 -0
- unicode_logic_kit/ilp/readback.py +389 -0
- unicode_logic_kit/ilp/separation.py +153 -0
- unicode_logic_kit/ilp/task.py +730 -0
- unicode_logic_kit/logic.py +163 -0
- unicode_logic_kit/mcp/__init__.py +28 -0
- unicode_logic_kit/mcp/__main__.py +5 -0
- unicode_logic_kit/mcp/chem_tools.py +1031 -0
- unicode_logic_kit/mcp/server.py +2453 -0
- unicode_logic_kit/mcp/syntax_spec.py +681 -0
- unicode_logic_kit/prob/__init__.py +53 -0
- unicode_logic_kit/prob/_bdd.py +225 -0
- unicode_logic_kit/prob/_column_gen.py +668 -0
- unicode_logic_kit/prob/distribution.py +686 -0
- unicode_logic_kit/prob/nilsson.py +470 -0
- unicode_logic_kit/py.typed +0 -0
- unicode_logic_kit/semantics/__init__.py +137 -0
- unicode_logic_kit/semantics/_modal_reject.py +156 -0
- unicode_logic_kit/semantics/action_models.py +466 -0
- unicode_logic_kit/semantics/asp_models.py +1200 -0
- unicode_logic_kit/semantics/conditional.py +580 -0
- unicode_logic_kit/semantics/dynamic_epistemic.py +95 -0
- unicode_logic_kit/semantics/free_logic.py +913 -0
- unicode_logic_kit/semantics/fuzzy.py +384 -0
- unicode_logic_kit/semantics/fuzzy_kripke.py +442 -0
- unicode_logic_kit/semantics/intuitionistic.py +581 -0
- unicode_logic_kit/semantics/kripke.py +1139 -0
- unicode_logic_kit/semantics/manyvalued.py +580 -0
- unicode_logic_kit/semantics/matrix.py +342 -0
- unicode_logic_kit/semantics/model_eval.py +1135 -0
- unicode_logic_kit/semantics/modelfinder.py +1036 -0
- unicode_logic_kit/semantics/nonmonotonic.py +372 -0
- unicode_logic_kit/semantics/relevant.py +331 -0
- unicode_logic_kit/semantics/secondorder.py +657 -0
- unicode_logic_kit/semantics/structures.py +352 -0
- unicode_logic_kit/semantics/tarski.py +975 -0
- unicode_logic_kit/semantics/team.py +315 -0
- unicode_logic_kit/semantics/team_translation.py +416 -0
- unicode_logic_kit/semantics/thirdorder.py +358 -0
- unicode_logic_kit/semantics/tnorm.py +85 -0
- unicode_logic_kit/semantics/truthtable.py +201 -0
- unicode_logic_kit-0.31.0.dist-info/METADATA +333 -0
- unicode_logic_kit-0.31.0.dist-info/RECORD +237 -0
- unicode_logic_kit-0.31.0.dist-info/WHEEL +4 -0
- 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)
|