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,1070 @@
|
|
|
1
|
+
"""Kit DRS → ACE text (``drs_to_ace``), with the round trip as the claim.
|
|
2
|
+
|
|
3
|
+
The reverse of :mod:`unicode_logic_kit.ace.mapping`: a DRS built from the
|
|
4
|
+
kit's condition inventory becomes an ACE text plus the user-lexicon entries
|
|
5
|
+
(APE ``-ulextext`` clauses) that make every content word parseable. The
|
|
6
|
+
correctness bar is NOT "natural English" — it is the machine-checked round
|
|
7
|
+
trip :func:`ace_round_trip`: the produced text, fed back through APE and
|
|
8
|
+
:func:`~unicode_logic_kit.ace.mapping.map_ace_drs`, must yield a DRS whose
|
|
9
|
+
:func:`~unicode_logic_kit.drt.export.drs_to_fol` is Z3-equivalent to the
|
|
10
|
+
input's. Everything below is in service of closing that loop, and the whole
|
|
11
|
+
mappable corpus closes it (pinned in ``tests/test_ace_verbalize.py``).
|
|
12
|
+
|
|
13
|
+
Design decisions, each probed against the live APE before being written:
|
|
14
|
+
|
|
15
|
+
- **Referent roles come from the kit's naming convention** — ``e*`` is an
|
|
16
|
+
event, ``g*`` a group, everything else an individual. The mapping names
|
|
17
|
+
referents this way by construction; hand-built DRSs must follow the same
|
|
18
|
+
convention (an "event" named ``x1`` is refused for having no noun, not
|
|
19
|
+
guessed at).
|
|
20
|
+
- **Every individual is introduced by a noun with a variable apposition**
|
|
21
|
+
("a man X1", "there is a man X1") and referenced as the bare variable
|
|
22
|
+
afterwards. Appositions survive every embedding APE was probed with,
|
|
23
|
+
while "there is … and …" under ``It is false that`` LOSES the second
|
|
24
|
+
conjunct from the negation scope — which is why a multi-clause negated
|
|
25
|
+
box is rewritten (below) rather than rendered as a conjunction.
|
|
26
|
+
- **The first unary condition over a referent is its noun; every further
|
|
27
|
+
unary becomes a predicative clause** ("X1 is a rich."). Predicative
|
|
28
|
+
indefinites come back as a fresh referent plus an equality — Z3-equal to
|
|
29
|
+
the plain unary atom — and using ONE lexical category (noun) for every
|
|
30
|
+
unary predicate avoids noun/adjective ambiguity in the generated
|
|
31
|
+
lexicon. The only adjectives the generator emits are the comparatives
|
|
32
|
+
(``Tall_comp_than`` → "taller than"), which have no noun form.
|
|
33
|
+
- **A negated box that needs more than one clause is rewritten**:
|
|
34
|
+
``¬∃R(C₁ ∧ … ∧ Cₙ)`` becomes ``∀R_front(C₁ ∧ … ∧ Cₙ₋₁ → ¬∃R_back Cₙ)``
|
|
35
|
+
— an if-then whose consequent is a one-clause negation. The rewrite is a
|
|
36
|
+
classical equivalence, and the round trip re-checks it per DRS.
|
|
37
|
+
- **Surface forms are mechanical and lexicon-defined, not English.** The
|
|
38
|
+
third-person singular and the plural add ``s``/``es``/``ies``, the
|
|
39
|
+
comparative adds ``er``/``r``/``ier``, underscores become hyphens
|
|
40
|
+
(``Bond_to`` → the verb ``bond-to``). "3 mans" is deliberate: the
|
|
41
|
+
emitted ``noun_pl(mans, man, neutr)`` entry DEFINES that surface, APE
|
|
42
|
+
parses it, and the logical symbol underneath is exactly ``man``. No
|
|
43
|
+
morphology is guessed; what the lexicon says is what the text uses.
|
|
44
|
+
- **Strict lower bounds only**: ``Card(g, <=, n)`` and ``Card(g, <, n)``
|
|
45
|
+
are refused — probed: APE reads "at most"/"less than" as the maximality
|
|
46
|
+
LIST condition (the distributive counting reading of the formula route),
|
|
47
|
+
so no ACE surface maps back to a plain upper-bound ``Card`` and the DRS
|
|
48
|
+
round trip cannot close. ``=``/``>=``/``>`` all close.
|
|
49
|
+
|
|
50
|
+
What is refused, by name (:class:`AceVerbalizationError`): a referent with
|
|
51
|
+
no noun, a value constant outside the right-hand side of an equality, a
|
|
52
|
+
constant or predicate whose name is not invertible (the surface must map
|
|
53
|
+
back to the SAME kit symbol through the mapping's own name rules — checked
|
|
54
|
+
programmatically per name, never assumed), binary predicates over
|
|
55
|
+
individuals other than ``Of``/``*_comp_than`` (an ACE verb always carries
|
|
56
|
+
an event, so a 2-place kit atom cannot survive the trip as a 2-place
|
|
57
|
+
atom), events used as participants, groups outside the three probed shapes
|
|
58
|
+
(counted plural, coordination, "each of"), and complex conditions nested
|
|
59
|
+
deeper than the probed fragment.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
from __future__ import annotations
|
|
63
|
+
|
|
64
|
+
import re
|
|
65
|
+
from dataclasses import dataclass, field
|
|
66
|
+
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
|
|
67
|
+
|
|
68
|
+
from ..drt.export import drs_to_fol
|
|
69
|
+
from ..drt.nodes import DRS, Card, Condition, Eq, Impl, Neg, Or, Part, Pred
|
|
70
|
+
from .mapping import _kit_predicate, _named_constant, ace_to_drs
|
|
71
|
+
from .reverse_modal import ModalBox, ModalImpl
|
|
72
|
+
from .runner import AceError
|
|
73
|
+
from ..fol._truth_constants import truth_constants_in
|
|
74
|
+
|
|
75
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
76
|
+
from .translate import AceFormula
|
|
77
|
+
|
|
78
|
+
__all__ = ["drs_to_ace", "formula_to_ace", "ace_round_trip", "AceText",
|
|
79
|
+
"AceRoundTrip", "AceVerbalizationError",
|
|
80
|
+
"modal_drs_to_ace", "modal_formula_to_ace", "modal_ace_round_trip",
|
|
81
|
+
"ModalAceRoundTrip"]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class AceVerbalizationError(AceError):
|
|
85
|
+
"""The DRS contains something no probed ACE surface maps back to."""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _refuse_truth_constants(formula, route: str) -> None:
|
|
89
|
+
"""Refuse, by name, a formula that holds a truth constant (``$true`` / ``⊤``,
|
|
90
|
+
``$false`` / ``⊥``).
|
|
91
|
+
|
|
92
|
+
ACE has no sentence that is true or false of itself: every ACE sentence says
|
|
93
|
+
something about the individuals a noun phrase introduces, and a DRS has no
|
|
94
|
+
condition that is a constant. The refusal is the one the formula routes already
|
|
95
|
+
give for anything outside the standard translation's image
|
|
96
|
+
(:class:`~unicode_logic_kit.drt.reverse.FolToDrsError`), with the reason that is
|
|
97
|
+
the constant's own rather than a predicate-naming rule.
|
|
98
|
+
"""
|
|
99
|
+
found = truth_constants_in([formula])
|
|
100
|
+
if not found:
|
|
101
|
+
return
|
|
102
|
+
from ..drt.reverse import FolToDrsError
|
|
103
|
+
|
|
104
|
+
names = " and ".join(atom.predicate for atom in found)
|
|
105
|
+
raise FolToDrsError(
|
|
106
|
+
f"{route}: the truth constant {names} has no ACE sentence: Attempto Controlled "
|
|
107
|
+
"English has no sentence that is true or false by itself (every ACE sentence "
|
|
108
|
+
"says something about the individuals a noun phrase introduces), so no DRS "
|
|
109
|
+
"condition stands for a constant. Write the formula without the constant, or "
|
|
110
|
+
"verbalise it with unicode_logic_kit.fol.to_english, which reads it as "
|
|
111
|
+
"'truth' / 'falsity'.")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class AceText:
|
|
116
|
+
"""The verbalization: ``text`` is the ACE sentences, ``ulex`` the APE
|
|
117
|
+
user-lexicon clauses (``-ulextext`` format, one per line). The
|
|
118
|
+
generator always supplies its content words rather than relying on
|
|
119
|
+
APE's built-in lexicon; duplicates with built-ins are harmless
|
|
120
|
+
(probed)."""
|
|
121
|
+
|
|
122
|
+
text: str
|
|
123
|
+
ulex: str
|
|
124
|
+
|
|
125
|
+
def __str__(self) -> str: # pragma: no cover — convenience
|
|
126
|
+
return self.text
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass(frozen=True)
|
|
130
|
+
class AceRoundTrip:
|
|
131
|
+
"""One closed (or failed) loop: DRS → ACE → APE → DRS, judged by Z3."""
|
|
132
|
+
|
|
133
|
+
verbalization: AceText
|
|
134
|
+
back: DRS
|
|
135
|
+
equivalent: bool
|
|
136
|
+
detail: str = ""
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# Mechanical morphology — lexicon-defined, never guessed (module docstring)
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def _s_form(base: str) -> str:
|
|
144
|
+
if re.search(r"(?:[sxz]|ch|sh)$", base):
|
|
145
|
+
return base + "es"
|
|
146
|
+
if re.search(r"[^aeiou]y$", base):
|
|
147
|
+
return base[:-1] + "ies"
|
|
148
|
+
return base + "s"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _er_form(base: str) -> str:
|
|
152
|
+
if base.endswith("e"):
|
|
153
|
+
return base + "r"
|
|
154
|
+
if re.search(r"[^aeiou]y$", base):
|
|
155
|
+
return base[:-1] + "ier"
|
|
156
|
+
return base + "er"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
_PLAIN_ATOM = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _atom(s: str) -> str:
|
|
163
|
+
"""Quote a Prolog atom for a ulex clause when needed."""
|
|
164
|
+
return s if _PLAIN_ATOM.match(s) else f"'{s}'"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _vowel_start(word: str) -> bool:
|
|
168
|
+
return bool(word) and word[0].lower() in "aeiou"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
_COMP_SUFFIX = "_comp_than"
|
|
172
|
+
_REFERENT_RE = re.compile(r"^[a-z][0-9]*$")
|
|
173
|
+
_INT_CONST = re.compile(r"^c_(\d+)$")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _is_event(name: str) -> bool:
|
|
177
|
+
return bool(re.match(r"^e[0-9]*$", name))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _is_group(name: str) -> bool:
|
|
181
|
+
return bool(re.match(r"^g[0-9]*$", name))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
# Lexicon collector
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
class _Lexicon:
|
|
189
|
+
def __init__(self) -> None:
|
|
190
|
+
self.entries: Set[str] = set()
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
def _logical(kit_name: str) -> str:
|
|
194
|
+
logical = kit_name[0].lower() + kit_name[1:]
|
|
195
|
+
if _kit_predicate(logical) != kit_name:
|
|
196
|
+
raise AceVerbalizationError(
|
|
197
|
+
f"drs_to_ace: predicate {kit_name!r} is not invertible — "
|
|
198
|
+
f"the round trip would rename it to "
|
|
199
|
+
f"{_kit_predicate(logical)!r}")
|
|
200
|
+
return logical
|
|
201
|
+
|
|
202
|
+
@staticmethod
|
|
203
|
+
def _surface(logical: str) -> str:
|
|
204
|
+
return logical.replace("_", "-")
|
|
205
|
+
|
|
206
|
+
def noun(self, kit_name: str, plural: bool = False) -> str:
|
|
207
|
+
logical = self._logical(kit_name)
|
|
208
|
+
base = self._surface(logical)
|
|
209
|
+
if plural:
|
|
210
|
+
form = _s_form(base)
|
|
211
|
+
self.entries.add(
|
|
212
|
+
f"noun_pl({_atom(form)}, {_atom(logical)}, neutr).")
|
|
213
|
+
return form
|
|
214
|
+
self.entries.add(f"noun_sg({_atom(base)}, {_atom(logical)}, neutr).")
|
|
215
|
+
return base
|
|
216
|
+
|
|
217
|
+
def verb(self, kit_name: str, participants: int, plural: bool) -> str:
|
|
218
|
+
logical = self._logical(kit_name)
|
|
219
|
+
base = self._surface(logical)
|
|
220
|
+
kind = {1: "iv", 2: "tv", 3: "dv"}.get(participants)
|
|
221
|
+
if kind is None:
|
|
222
|
+
raise AceVerbalizationError(
|
|
223
|
+
f"drs_to_ace: verb {kit_name!r} with {participants} "
|
|
224
|
+
"participants — ACE verbs take one to three")
|
|
225
|
+
prep = ", to" if kind == "dv" else ""
|
|
226
|
+
form = base if plural else _s_form(base)
|
|
227
|
+
mode = "infpl" if plural else "finsg"
|
|
228
|
+
self.entries.add(
|
|
229
|
+
f"{kind}_{mode}({_atom(form)}, {_atom(logical)}{prep}).")
|
|
230
|
+
return form
|
|
231
|
+
|
|
232
|
+
def comparative(self, kit_name: str) -> str:
|
|
233
|
+
logical = self._logical(kit_name[:-len(_COMP_SUFFIX)])
|
|
234
|
+
base = self._surface(logical)
|
|
235
|
+
form = _er_form(base)
|
|
236
|
+
self.entries.add(f"adj_itr({_atom(base)}, {_atom(logical)}).")
|
|
237
|
+
self.entries.add(f"adj_itr_comp({_atom(form)}, {_atom(logical)}).")
|
|
238
|
+
return form
|
|
239
|
+
|
|
240
|
+
def adverb(self, kit_name: str) -> str:
|
|
241
|
+
logical = self._logical(kit_name)
|
|
242
|
+
base = self._surface(logical)
|
|
243
|
+
self.entries.add(f"adv({_atom(base)}, {_atom(logical)}).")
|
|
244
|
+
return base
|
|
245
|
+
|
|
246
|
+
def preposition(self, kit_name: str) -> str:
|
|
247
|
+
logical = self._logical(kit_name)
|
|
248
|
+
base = self._surface(logical)
|
|
249
|
+
self.entries.add(f"prep({_atom(base)}, {_atom(logical)}).")
|
|
250
|
+
return base
|
|
251
|
+
|
|
252
|
+
def proper_name(self, constant: str) -> str:
|
|
253
|
+
surface = constant[0].upper() + constant[1:]
|
|
254
|
+
if _named_constant(surface) != constant:
|
|
255
|
+
raise AceVerbalizationError(
|
|
256
|
+
f"drs_to_ace: constant {constant!r} is not invertible — "
|
|
257
|
+
f"the round trip would rename it to "
|
|
258
|
+
f"{_named_constant(surface)!r}")
|
|
259
|
+
self.entries.add(
|
|
260
|
+
f"pn_sg({_atom(surface)}, {_atom(surface)}, neutr).")
|
|
261
|
+
return surface
|
|
262
|
+
|
|
263
|
+
def text(self) -> str:
|
|
264
|
+
return "\n".join(sorted(self.entries))
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
# Structure helpers
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
def _sub_boxes(cond: Condition) -> Tuple[DRS, ...]:
|
|
272
|
+
if isinstance(cond, Neg):
|
|
273
|
+
return (cond.drs,)
|
|
274
|
+
if isinstance(cond, Impl):
|
|
275
|
+
return (cond.antecedent, cond.consequent)
|
|
276
|
+
if isinstance(cond, Or):
|
|
277
|
+
return (cond.left, cond.right)
|
|
278
|
+
return ()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _cond_names(cond: Condition) -> Set[str]:
|
|
282
|
+
"""Every name (referent or constant) a condition touches, sub-boxes'
|
|
283
|
+
FREE names included."""
|
|
284
|
+
if isinstance(cond, Pred):
|
|
285
|
+
return set(cond.args)
|
|
286
|
+
if isinstance(cond, Eq):
|
|
287
|
+
return {cond.a, cond.b}
|
|
288
|
+
if isinstance(cond, Card):
|
|
289
|
+
return {cond.ref}
|
|
290
|
+
if isinstance(cond, Part):
|
|
291
|
+
return {cond.member, cond.group}
|
|
292
|
+
names: Set[str] = set()
|
|
293
|
+
for box in _sub_boxes(cond):
|
|
294
|
+
names |= _box_free_names(box)
|
|
295
|
+
return names
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _box_free_names(box: DRS) -> Set[str]:
|
|
299
|
+
names: Set[str] = set()
|
|
300
|
+
for cond in box.conditions:
|
|
301
|
+
names |= _cond_names(cond)
|
|
302
|
+
return names - set(box.referents)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _all_referents(box: DRS) -> Set[str]:
|
|
306
|
+
refs = set(box.referents)
|
|
307
|
+
for cond in box.conditions:
|
|
308
|
+
for sub in _sub_boxes(cond):
|
|
309
|
+
refs |= _all_referents(sub)
|
|
310
|
+
return refs
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _membership_duplex(cond: Condition, group: str) -> Optional[str]:
|
|
314
|
+
"""The duplex variable if ``cond`` is ``[v | Part_of(v, g)] => …``."""
|
|
315
|
+
if not isinstance(cond, Impl):
|
|
316
|
+
return None
|
|
317
|
+
ante = cond.antecedent
|
|
318
|
+
if (len(ante.referents) == 1 and len(ante.conditions) == 1
|
|
319
|
+
and isinstance(ante.conditions[0], Part)
|
|
320
|
+
and ante.conditions[0].member == ante.referents[0]
|
|
321
|
+
and ante.conditions[0].group == group):
|
|
322
|
+
return ante.referents[0]
|
|
323
|
+
return None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _card_words(card: Card) -> str:
|
|
327
|
+
if card.op == "=":
|
|
328
|
+
return str(card.n)
|
|
329
|
+
if card.op == ">=":
|
|
330
|
+
return f"at least {card.n}"
|
|
331
|
+
if card.op == ">":
|
|
332
|
+
return f"more than {card.n}"
|
|
333
|
+
# <=, <: probed — APE reads "at most"/"less than" as the maximality
|
|
334
|
+
# list condition, so no surface maps back to a plain upper-bound Card.
|
|
335
|
+
raise AceVerbalizationError(
|
|
336
|
+
f"drs_to_ace: Card op {card.op!r} has no ACE surface that survives "
|
|
337
|
+
"the DRS round trip — APE reads upper bounds as the maximality "
|
|
338
|
+
"list, which is the formula route's counting reading")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
# Per-referent state
|
|
343
|
+
# ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
@dataclass
|
|
346
|
+
class _Np:
|
|
347
|
+
kind: str # "individual" | "plural" | "coord" | "each"
|
|
348
|
+
noun: Optional[str] = None # kit name of the introducing noun
|
|
349
|
+
extras: List[str] = field(default_factory=list)
|
|
350
|
+
of_target: Optional[str] = None
|
|
351
|
+
card: Optional[Card] = None
|
|
352
|
+
members: List[str] = field(default_factory=list) # kit constants
|
|
353
|
+
duplex: Optional[Impl] = None
|
|
354
|
+
var: Optional[str] = None # "X1" once realized
|
|
355
|
+
introduced: bool = False
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# ---------------------------------------------------------------------------
|
|
359
|
+
# The generator
|
|
360
|
+
# ---------------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
def drs_to_ace(drs: DRS) -> AceText:
|
|
363
|
+
"""Verbalize a kit DRS as ACE text plus the lexicon that carries it.
|
|
364
|
+
|
|
365
|
+
See the module docstring for the covered fragment and the refusals;
|
|
366
|
+
:func:`ace_round_trip` is the live self-check that the text means what
|
|
367
|
+
the DRS means.
|
|
368
|
+
"""
|
|
369
|
+
drs.validate()
|
|
370
|
+
v = _Verbalizer(drs)
|
|
371
|
+
clauses = _box_clauses(drs, v, env={}, depth=0)
|
|
372
|
+
sentences = [c[0].upper() + c[1:] + "." for c in clauses]
|
|
373
|
+
return AceText(text=" ".join(sentences), ulex=v.lexicon.text())
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class _Verbalizer:
|
|
377
|
+
"""Shared state across the recursion: lexicon, variable counter, the
|
|
378
|
+
set of declared referent names (for the referent/constant split)."""
|
|
379
|
+
|
|
380
|
+
def __init__(self, root: DRS):
|
|
381
|
+
self.lexicon = _Lexicon()
|
|
382
|
+
self._var_n = 0
|
|
383
|
+
self.declared = {r for r in _all_referents(root)
|
|
384
|
+
if _REFERENT_RE.match(r)}
|
|
385
|
+
|
|
386
|
+
def fresh_var(self) -> str:
|
|
387
|
+
self._var_n += 1
|
|
388
|
+
return f"X{self._var_n}"
|
|
389
|
+
|
|
390
|
+
def is_ref(self, name: str) -> bool:
|
|
391
|
+
return name in self.declared
|
|
392
|
+
|
|
393
|
+
def constant_np(self, name: str) -> str:
|
|
394
|
+
"""A constant in NP (participant) position: proper names only."""
|
|
395
|
+
if name.startswith("c_"):
|
|
396
|
+
raise AceVerbalizationError(
|
|
397
|
+
f"drs_to_ace: value constant {name!r} outside the "
|
|
398
|
+
"right-hand side of an equality — no probed ACE surface "
|
|
399
|
+
"puts a value in a participant position")
|
|
400
|
+
return self.lexicon.proper_name(name)
|
|
401
|
+
|
|
402
|
+
def constant_value(self, name: str) -> str:
|
|
403
|
+
"""A constant on the right of an equality: value or proper name."""
|
|
404
|
+
m = _INT_CONST.match(name)
|
|
405
|
+
if m:
|
|
406
|
+
return m.group(1)
|
|
407
|
+
if name.startswith("c_"):
|
|
408
|
+
tail = name[2:]
|
|
409
|
+
if re.match(r"^m\d", tail) or re.match(r"^\d+p\d", tail):
|
|
410
|
+
raise AceVerbalizationError(
|
|
411
|
+
f"drs_to_ace: value constant {name!r} — negative and "
|
|
412
|
+
"real values are outside the measured corpus")
|
|
413
|
+
# The string form: string('Tail') → c_Tail closes the loop
|
|
414
|
+
# even when the constant was born as a protected proper name
|
|
415
|
+
# (c_E1) — the renamer gives both the same kit spelling.
|
|
416
|
+
return f'"{tail}"'
|
|
417
|
+
return self.lexicon.proper_name(name)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _box_clauses(box: DRS, v: _Verbalizer, env: Dict[str, _Np],
|
|
421
|
+
depth: int, modality: Optional[str] = None) -> List[str]:
|
|
422
|
+
"""One box → its clause list. ``env`` is MUTATED (introductions land in
|
|
423
|
+
it); callers that need scope isolation pass a copy — see
|
|
424
|
+
:func:`_complex_clause`, where an if-then shares one env between
|
|
425
|
+
antecedent and consequent while or-branches and negations get copies.
|
|
426
|
+
|
|
427
|
+
``modality`` (one of "must"/"can"/"should"/"may", ACE-7) is threaded
|
|
428
|
+
through to :func:`verb_clause` only — a modal auxiliary sits INSIDE the
|
|
429
|
+
verb phrase (probed: "John must wait.", not a sentence paraphrase), so
|
|
430
|
+
it never touches NP realization, groups, equalities or nested complexes.
|
|
431
|
+
``None`` (every EXISTING call site) reproduces the pre-ACE-7 behavior
|
|
432
|
+
exactly. Callers that pass a modality (:mod:`unicode_logic_kit.ace.reverse_modal`'s
|
|
433
|
+
two wrappers only) must hand this function a box that renders to EXACTLY
|
|
434
|
+
ONE clause — the only shape measured under a modal auxiliary; see
|
|
435
|
+
:func:`modal_drs_to_ace`, which enforces that after calling this.
|
|
436
|
+
"""
|
|
437
|
+
preds = [c for c in box.conditions if isinstance(c, Pred)]
|
|
438
|
+
eqs = [c for c in box.conditions if isinstance(c, Eq)]
|
|
439
|
+
cards = [c for c in box.conditions if isinstance(c, Card)]
|
|
440
|
+
parts = [c for c in box.conditions if isinstance(c, Part)]
|
|
441
|
+
complexes = [c for c in box.conditions if isinstance(c, (Neg, Impl, Or))]
|
|
442
|
+
|
|
443
|
+
events = [r for r in box.referents if _is_event(r)]
|
|
444
|
+
groups = [r for r in box.referents if _is_group(r)]
|
|
445
|
+
individuals = [r for r in box.referents
|
|
446
|
+
if not _is_event(r) and not _is_group(r)]
|
|
447
|
+
|
|
448
|
+
# -- groups: classify each declared group into one of the three shapes
|
|
449
|
+
consumed: Set[int] = set()
|
|
450
|
+
for g in groups:
|
|
451
|
+
g_cards = [c for c in cards if c.ref == g]
|
|
452
|
+
g_parts = [p for p in parts if p.group == g]
|
|
453
|
+
duplex = next((c for c in complexes
|
|
454
|
+
if _membership_duplex(c, g) is not None), None)
|
|
455
|
+
if len(g_cards) != 1:
|
|
456
|
+
raise AceVerbalizationError(
|
|
457
|
+
f"drs_to_ace: group {g!r} needs exactly one Card condition")
|
|
458
|
+
card = g_cards[0]
|
|
459
|
+
np = _Np(kind="?", card=card)
|
|
460
|
+
if duplex is not None:
|
|
461
|
+
consumed.add(id(duplex))
|
|
462
|
+
np.duplex = duplex
|
|
463
|
+
dup_var = _membership_duplex(duplex, g)
|
|
464
|
+
cons = duplex.consequent
|
|
465
|
+
if (not g_parts and not cons.referents
|
|
466
|
+
and len(cons.conditions) == 1
|
|
467
|
+
and isinstance(cons.conditions[0], Pred)
|
|
468
|
+
and cons.conditions[0].args == (dup_var,)):
|
|
469
|
+
np.kind = "plural"
|
|
470
|
+
np.noun = cons.conditions[0].name
|
|
471
|
+
elif g_parts:
|
|
472
|
+
np.kind = "each"
|
|
473
|
+
np.members = [p.member for p in g_parts]
|
|
474
|
+
else:
|
|
475
|
+
raise AceVerbalizationError(
|
|
476
|
+
f"drs_to_ace: group {g!r}: the membership duplex fits "
|
|
477
|
+
"neither the counted-plural nor the each-of shape")
|
|
478
|
+
elif g_parts:
|
|
479
|
+
np.kind = "coord"
|
|
480
|
+
np.members = [p.member for p in g_parts]
|
|
481
|
+
else:
|
|
482
|
+
raise AceVerbalizationError(
|
|
483
|
+
f"drs_to_ace: group {g!r} has neither members nor a "
|
|
484
|
+
"membership duplex — no noun to verbalize")
|
|
485
|
+
if np.kind in ("coord", "each"):
|
|
486
|
+
if card.op != "=" or card.n != len(np.members):
|
|
487
|
+
raise AceVerbalizationError(
|
|
488
|
+
f"drs_to_ace: group {g!r}: Card({g}, {card.op}, "
|
|
489
|
+
f"{card.n}) does not close over its "
|
|
490
|
+
f"{len(np.members)} listed members")
|
|
491
|
+
if any(v.is_ref(m) for m in np.members):
|
|
492
|
+
raise AceVerbalizationError(
|
|
493
|
+
f"drs_to_ace: group {g!r} lists a referent as member "
|
|
494
|
+
"— only constant members are in the measured fragment")
|
|
495
|
+
env[g] = np
|
|
496
|
+
for p in parts:
|
|
497
|
+
if p.group not in env or env[p.group].kind not in ("coord", "each"):
|
|
498
|
+
raise AceVerbalizationError(
|
|
499
|
+
f"drs_to_ace: {p.to_box_notation()} outside a declared "
|
|
500
|
+
"group's cluster")
|
|
501
|
+
for c in cards:
|
|
502
|
+
if c.ref not in env or env[c.ref].kind == "individual":
|
|
503
|
+
raise AceVerbalizationError(
|
|
504
|
+
f"drs_to_ace: {c.to_box_notation()} outside a declared "
|
|
505
|
+
"group's cluster")
|
|
506
|
+
|
|
507
|
+
# -- individuals: nouns, extras, of-attachments
|
|
508
|
+
for r in individuals:
|
|
509
|
+
unaries = [p for p in preds if p.args == (r,)]
|
|
510
|
+
np = _Np(kind="individual")
|
|
511
|
+
if unaries:
|
|
512
|
+
np.noun = unaries[0].name
|
|
513
|
+
np.extras = [p.name for p in unaries[1:]]
|
|
514
|
+
env[r] = np
|
|
515
|
+
for p in preds:
|
|
516
|
+
if (p.name == "Of" and len(p.args) == 2
|
|
517
|
+
and not _is_event(p.args[0])):
|
|
518
|
+
anchor = p.args[0]
|
|
519
|
+
if anchor not in env or env[anchor].kind != "individual" \
|
|
520
|
+
or env[anchor].introduced:
|
|
521
|
+
raise AceVerbalizationError(
|
|
522
|
+
f"drs_to_ace: Of anchor {anchor!r} is not an "
|
|
523
|
+
"individual declared in the same box")
|
|
524
|
+
if env[anchor].of_target is not None:
|
|
525
|
+
raise AceVerbalizationError(
|
|
526
|
+
f"drs_to_ace: {anchor!r} carries more than one Of "
|
|
527
|
+
"relation — outside the measured fragment")
|
|
528
|
+
env[anchor].of_target = p.args[1]
|
|
529
|
+
# A noun-less individual can still ride on an equality: the mapping's
|
|
530
|
+
# copula rule produces exactly this shape for comparatives —
|
|
531
|
+
# Eq(john, x1) ∧ Tall_comp_than(x1, mary) — where x1 exists only to be
|
|
532
|
+
# John. Alias it to the other side and CONSUME that equality (emitting
|
|
533
|
+
# both the alias and the clause would be redundant but harmless; the
|
|
534
|
+
# alias alone keeps the text one sentence).
|
|
535
|
+
consumed_eqs: Set[int] = set()
|
|
536
|
+
for r in individuals:
|
|
537
|
+
if env[r].noun is not None:
|
|
538
|
+
continue
|
|
539
|
+
alias = None
|
|
540
|
+
for eq in eqs:
|
|
541
|
+
if id(eq) in consumed_eqs:
|
|
542
|
+
continue
|
|
543
|
+
other = eq.b if eq.a == r else eq.a if eq.b == r else None
|
|
544
|
+
if other is None or other == r:
|
|
545
|
+
continue
|
|
546
|
+
if not v.is_ref(other) or env.get(other, _Np("?")).noun is not None:
|
|
547
|
+
alias = other
|
|
548
|
+
consumed_eqs.add(id(eq))
|
|
549
|
+
break
|
|
550
|
+
if alias is None:
|
|
551
|
+
raise AceVerbalizationError(
|
|
552
|
+
f"drs_to_ace: referent {r!r} has no noun (no unary "
|
|
553
|
+
"condition) and no equality to ride on — nothing to "
|
|
554
|
+
"introduce it with. Events must be named e*, groups g*.")
|
|
555
|
+
env[r].kind = "alias"
|
|
556
|
+
env[r].of_target = None
|
|
557
|
+
env[r].noun = alias # stores the aliased NAME, not a noun
|
|
558
|
+
env[r].introduced = True
|
|
559
|
+
|
|
560
|
+
# -- classify the event-anchored and remaining predicates
|
|
561
|
+
verb_preds: Dict[str, List[Pred]] = {e: [] for e in events}
|
|
562
|
+
comparatives: List[Pred] = []
|
|
563
|
+
predicative_consts: List[Pred] = []
|
|
564
|
+
for p in preds:
|
|
565
|
+
if p.args and _is_event(p.args[0]):
|
|
566
|
+
if p.args[0] not in verb_preds:
|
|
567
|
+
raise AceVerbalizationError(
|
|
568
|
+
f"drs_to_ace: event {p.args[0]!r} is not declared in "
|
|
569
|
+
"the box of its verb")
|
|
570
|
+
verb_preds[p.args[0]].append(p)
|
|
571
|
+
continue
|
|
572
|
+
if p.name.endswith(_COMP_SUFFIX) and len(p.args) == 2:
|
|
573
|
+
comparatives.append(p)
|
|
574
|
+
continue
|
|
575
|
+
if p.name == "Of" and len(p.args) == 2:
|
|
576
|
+
continue # attached above
|
|
577
|
+
if len(p.args) == 1 and not v.is_ref(p.args[0]):
|
|
578
|
+
predicative_consts.append(p) # Man(john) → "John is a man."
|
|
579
|
+
continue
|
|
580
|
+
if len(p.args) == 1:
|
|
581
|
+
continue # nouns/extras, consumed above
|
|
582
|
+
raise AceVerbalizationError(
|
|
583
|
+
f"drs_to_ace: {p.to_box_notation()} — a {len(p.args)}-place "
|
|
584
|
+
"predicate over individuals has no event, and an ACE verb "
|
|
585
|
+
"always carries one; only Of and *_comp_than survive the trip")
|
|
586
|
+
for e in events:
|
|
587
|
+
others = [c for c in box.conditions
|
|
588
|
+
if not (isinstance(c, Pred) and c.args
|
|
589
|
+
and c.args[0] == e)]
|
|
590
|
+
if any(e in _cond_names(c) for c in others):
|
|
591
|
+
raise AceVerbalizationError(
|
|
592
|
+
f"drs_to_ace: event {e!r} used outside its own verb "
|
|
593
|
+
"conditions — not in the measured fragment")
|
|
594
|
+
if not any(len(p.args) >= 2 for p in verb_preds[e]):
|
|
595
|
+
raise AceVerbalizationError(
|
|
596
|
+
f"drs_to_ace: event {e!r} has no verb condition")
|
|
597
|
+
|
|
598
|
+
# -- NP realization ----------------------------------------------------
|
|
599
|
+
def np_first(r: str) -> str:
|
|
600
|
+
np = env[r]
|
|
601
|
+
np.var = v.fresh_var()
|
|
602
|
+
np.introduced = True
|
|
603
|
+
if np.kind == "plural":
|
|
604
|
+
noun = v.lexicon.noun(np.noun, plural=True)
|
|
605
|
+
return f"{_card_words(np.card)} {noun} {np.var}"
|
|
606
|
+
noun = v.lexicon.noun(np.noun)
|
|
607
|
+
det = "an" if _vowel_start(noun) else "a"
|
|
608
|
+
words = [det, noun, np.var]
|
|
609
|
+
if np.of_target is not None:
|
|
610
|
+
words += ["of", np_use(np.of_target)]
|
|
611
|
+
return " ".join(words)
|
|
612
|
+
|
|
613
|
+
def np_use(name: str) -> str:
|
|
614
|
+
if not v.is_ref(name):
|
|
615
|
+
return v.constant_np(name)
|
|
616
|
+
np = env.get(name)
|
|
617
|
+
if np is None:
|
|
618
|
+
raise AceVerbalizationError(
|
|
619
|
+
f"drs_to_ace: referent {name!r} used but not accessible")
|
|
620
|
+
if np.kind == "alias":
|
|
621
|
+
return (np_use(np.noun) if v.is_ref(np.noun)
|
|
622
|
+
else v.constant_np(np.noun))
|
|
623
|
+
if np.kind == "coord":
|
|
624
|
+
if np.introduced:
|
|
625
|
+
raise AceVerbalizationError(
|
|
626
|
+
"drs_to_ace: coordination group used more than once — "
|
|
627
|
+
"a repeated 'John and Mary' would denote a NEW group")
|
|
628
|
+
np.introduced = True
|
|
629
|
+
return " and ".join(v.constant_np(m) for m in np.members)
|
|
630
|
+
if not np.introduced:
|
|
631
|
+
return np_first(name)
|
|
632
|
+
return np.var
|
|
633
|
+
|
|
634
|
+
def is_plural_subject(name: str) -> bool:
|
|
635
|
+
np = env.get(name)
|
|
636
|
+
return np is not None and np.kind in ("plural", "coord")
|
|
637
|
+
|
|
638
|
+
def verb_clause(e: str) -> str:
|
|
639
|
+
ordered = verb_preds[e]
|
|
640
|
+
verb = next(p for p in ordered if len(p.args) >= 2)
|
|
641
|
+
participants = verb.args[1:]
|
|
642
|
+
if len(participants) > 3:
|
|
643
|
+
raise AceVerbalizationError(
|
|
644
|
+
f"drs_to_ace: verb {verb.name!r} with {len(participants)} "
|
|
645
|
+
"participants — ACE verbs take one to three")
|
|
646
|
+
# Under a modal auxiliary the verb always takes the bare/infinitive
|
|
647
|
+
# lexicon form — probed live: "must florb"/"must chase" parse only
|
|
648
|
+
# against iv_infpl/tv_infpl, "must florbs" is refused even though
|
|
649
|
+
# plain "florbs" alone is fine (test_ace_modal_verbalize.py).
|
|
650
|
+
plural = True if modality is not None else is_plural_subject(participants[0])
|
|
651
|
+
words = [np_use(participants[0])]
|
|
652
|
+
if modality is not None:
|
|
653
|
+
words.append(modality)
|
|
654
|
+
words.append(v.lexicon.verb(verb.name, len(participants), plural))
|
|
655
|
+
if len(participants) >= 2:
|
|
656
|
+
words.append(np_use(participants[1]))
|
|
657
|
+
if len(participants) == 3:
|
|
658
|
+
words += ["to", np_use(participants[2])]
|
|
659
|
+
for m in ordered:
|
|
660
|
+
if m is verb:
|
|
661
|
+
continue
|
|
662
|
+
if len(m.args) == 1:
|
|
663
|
+
words.append(v.lexicon.adverb(m.name))
|
|
664
|
+
elif len(m.args) == 2:
|
|
665
|
+
words += [v.lexicon.preposition(m.name), np_use(m.args[1])]
|
|
666
|
+
else:
|
|
667
|
+
raise AceVerbalizationError(
|
|
668
|
+
f"drs_to_ace: modifier {m.to_box_notation()} with "
|
|
669
|
+
f"{len(m.args) - 1} objects is not in the measured "
|
|
670
|
+
"fragment")
|
|
671
|
+
return " ".join(words)
|
|
672
|
+
|
|
673
|
+
# -- assemble the clause list -----------------------------------------
|
|
674
|
+
clauses: List[str] = []
|
|
675
|
+
|
|
676
|
+
# 1. plural groups always introduce first ("there are …")
|
|
677
|
+
for g in groups:
|
|
678
|
+
if env[g].kind == "plural":
|
|
679
|
+
clauses.append("there are " + np_first(g))
|
|
680
|
+
|
|
681
|
+
# 2. of-carrying individuals (and their referent targets) introduce
|
|
682
|
+
# before any use, target-first — "a dog X1 of X2" needs X2 known.
|
|
683
|
+
def intro_of(r: str, visiting: Set[str]) -> None:
|
|
684
|
+
np = env[r]
|
|
685
|
+
if np.introduced:
|
|
686
|
+
return
|
|
687
|
+
if r in visiting:
|
|
688
|
+
raise AceVerbalizationError(
|
|
689
|
+
f"drs_to_ace: circular Of chain at {r!r}")
|
|
690
|
+
visiting.add(r)
|
|
691
|
+
target = np.of_target
|
|
692
|
+
if target is not None and v.is_ref(target) \
|
|
693
|
+
and env[target].kind == "individual":
|
|
694
|
+
intro_of(target, visiting)
|
|
695
|
+
clauses.append("there is " + np_first(r))
|
|
696
|
+
|
|
697
|
+
for r in individuals:
|
|
698
|
+
if env[r].of_target is not None:
|
|
699
|
+
intro_of(r, set())
|
|
700
|
+
|
|
701
|
+
# 3. verb clauses; an each-of group renders as one distributed clause
|
|
702
|
+
for g in groups:
|
|
703
|
+
if env[g].kind == "each":
|
|
704
|
+
clauses.append(_each_of_clause(g, env[g], v))
|
|
705
|
+
others = [c for c in box.conditions
|
|
706
|
+
if id(c) != id(env[g].duplex)
|
|
707
|
+
and not (isinstance(c, (Card, Part))
|
|
708
|
+
and g in _cond_names(c))]
|
|
709
|
+
if any(g in _cond_names(c) for c in others):
|
|
710
|
+
raise AceVerbalizationError(
|
|
711
|
+
f"drs_to_ace: each-of group {g!r} used outside its "
|
|
712
|
+
"own cluster — not in the measured fragment")
|
|
713
|
+
for e in events:
|
|
714
|
+
clauses.append(verb_clause(e))
|
|
715
|
+
|
|
716
|
+
# 4. remaining introductions (referents used only in equalities,
|
|
717
|
+
# comparatives, sub-boxes — or not at all)
|
|
718
|
+
for r in individuals:
|
|
719
|
+
if not env[r].introduced:
|
|
720
|
+
clauses.append("there is " + np_first(r))
|
|
721
|
+
for g in groups:
|
|
722
|
+
if env[g].kind == "coord" and not env[g].introduced:
|
|
723
|
+
raise AceVerbalizationError(
|
|
724
|
+
f"drs_to_ace: coordination group {g!r} is not the "
|
|
725
|
+
"participant of any verb — nothing carries it")
|
|
726
|
+
|
|
727
|
+
# 5. predicative clauses for extra unaries and constant facts
|
|
728
|
+
for r in individuals:
|
|
729
|
+
for extra in env[r].extras:
|
|
730
|
+
noun = v.lexicon.noun(extra)
|
|
731
|
+
det = "an" if _vowel_start(noun) else "a"
|
|
732
|
+
clauses.append(f"{env[r].var} is {det} {noun}")
|
|
733
|
+
for p in predicative_consts:
|
|
734
|
+
noun = v.lexicon.noun(p.name)
|
|
735
|
+
det = "an" if _vowel_start(noun) else "a"
|
|
736
|
+
clauses.append(f"{v.constant_np(p.args[0])} is {det} {noun}")
|
|
737
|
+
|
|
738
|
+
# 6. equalities and comparatives (every referent is introduced by now)
|
|
739
|
+
def np_or_name(name: str) -> str:
|
|
740
|
+
return np_use(name) if v.is_ref(name) else v.constant_np(name)
|
|
741
|
+
|
|
742
|
+
for eq in eqs:
|
|
743
|
+
if id(eq) in consumed_eqs:
|
|
744
|
+
continue
|
|
745
|
+
left, right = eq.a, eq.b
|
|
746
|
+
if not v.is_ref(left) and v.is_ref(right):
|
|
747
|
+
left, right = right, left # values read best on the right
|
|
748
|
+
right_text = (np_use(right) if v.is_ref(right)
|
|
749
|
+
else v.constant_value(right))
|
|
750
|
+
clauses.append(f"{np_or_name(left)} is {right_text}")
|
|
751
|
+
for p in comparatives:
|
|
752
|
+
clauses.append(f"{np_or_name(p.args[0])} is "
|
|
753
|
+
f"{v.lexicon.comparative(p.name)} than "
|
|
754
|
+
f"{np_or_name(p.args[1])}")
|
|
755
|
+
|
|
756
|
+
# 7. complex conditions
|
|
757
|
+
for cond in complexes:
|
|
758
|
+
if id(cond) in consumed:
|
|
759
|
+
continue
|
|
760
|
+
clauses.append(_complex_clause(cond, v, env, depth))
|
|
761
|
+
|
|
762
|
+
if not clauses:
|
|
763
|
+
raise AceVerbalizationError(
|
|
764
|
+
"drs_to_ace: the box has no conditions to verbalize")
|
|
765
|
+
return clauses
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def _each_of_clause(g: str, np: _Np, v: _Verbalizer) -> str:
|
|
769
|
+
""""each of John and Mary waits" — the duplex consequent as one clause
|
|
770
|
+
whose subject is the spoken member list. The distributed box must hold
|
|
771
|
+
exactly one verb over the member variable (plus event-anchored
|
|
772
|
+
modifiers with constant objects) — anything else is refused."""
|
|
773
|
+
dup_var = _membership_duplex(np.duplex, g)
|
|
774
|
+
cons = np.duplex.consequent
|
|
775
|
+
cons_events = [r for r in cons.referents if _is_event(r)]
|
|
776
|
+
if len(cons_events) != 1 or len(cons.referents) != 1:
|
|
777
|
+
raise AceVerbalizationError(
|
|
778
|
+
f"drs_to_ace: each-of group {g!r}: the distributed box must "
|
|
779
|
+
"declare exactly the verb's event")
|
|
780
|
+
event = cons_events[0]
|
|
781
|
+
if not all(isinstance(c, Pred) and c.args and c.args[0] == event
|
|
782
|
+
for c in cons.conditions):
|
|
783
|
+
raise AceVerbalizationError(
|
|
784
|
+
f"drs_to_ace: each-of group {g!r}: the distributed box holds "
|
|
785
|
+
"conditions beside its verb — not in the measured fragment")
|
|
786
|
+
ordered = list(cons.conditions)
|
|
787
|
+
verb = next((p for p in ordered if len(p.args) >= 2), None)
|
|
788
|
+
if verb is None or verb.args[1] != dup_var:
|
|
789
|
+
raise AceVerbalizationError(
|
|
790
|
+
f"drs_to_ace: each-of group {g!r}: the distributed verb must "
|
|
791
|
+
"take the member variable as its subject")
|
|
792
|
+
words = ["each of",
|
|
793
|
+
" and ".join(v.constant_np(m) for m in np.members),
|
|
794
|
+
v.lexicon.verb(verb.name, len(verb.args) - 1, plural=False)]
|
|
795
|
+
objects = list(verb.args[2:])
|
|
796
|
+
if objects:
|
|
797
|
+
words.append(v.constant_np(objects[0]))
|
|
798
|
+
if len(objects) == 2:
|
|
799
|
+
words += ["to", v.constant_np(objects[1])]
|
|
800
|
+
for m in ordered:
|
|
801
|
+
if m is verb:
|
|
802
|
+
continue
|
|
803
|
+
if len(m.args) == 1:
|
|
804
|
+
words.append(v.lexicon.adverb(m.name))
|
|
805
|
+
elif len(m.args) == 2:
|
|
806
|
+
words += [v.lexicon.preposition(m.name),
|
|
807
|
+
v.constant_np(m.args[1])]
|
|
808
|
+
else:
|
|
809
|
+
raise AceVerbalizationError(
|
|
810
|
+
f"drs_to_ace: each-of group {g!r}: modifier shape not in "
|
|
811
|
+
"the measured fragment")
|
|
812
|
+
np.introduced = True
|
|
813
|
+
return " ".join(words)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
def _complex_clause(cond: Condition, v: _Verbalizer, env: Dict[str, _Np],
|
|
817
|
+
depth: int) -> str:
|
|
818
|
+
"""One embedded box structure as ONE clause. Scope handling probed:
|
|
819
|
+
an if-then SHARES one env copy between antecedent and consequent (the
|
|
820
|
+
antecedent's referents stay visible after "then"), or-branches and
|
|
821
|
+
negations are islands and get their own copies."""
|
|
822
|
+
if depth > 1:
|
|
823
|
+
raise AceVerbalizationError(
|
|
824
|
+
"drs_to_ace: complex conditions nested deeper than the probed "
|
|
825
|
+
"fragment (one embedded level)")
|
|
826
|
+
if isinstance(cond, Neg):
|
|
827
|
+
mark = v._var_n
|
|
828
|
+
inner = _box_clauses(cond.drs, v, dict(env), depth + 1)
|
|
829
|
+
if len(inner) == 1:
|
|
830
|
+
return "it is false that " + inner[0]
|
|
831
|
+
v._var_n = mark # the discarded attempt must not burn variables
|
|
832
|
+
return _rewrite_neg(cond, v, env, depth)
|
|
833
|
+
if isinstance(cond, Impl):
|
|
834
|
+
shared = dict(env)
|
|
835
|
+
ante = _box_clauses(cond.antecedent, v, shared, depth + 1)
|
|
836
|
+
cons = _box_clauses(cond.consequent, v, shared, depth + 1)
|
|
837
|
+
return "if " + " and ".join(ante) + " then " + " and ".join(cons)
|
|
838
|
+
if isinstance(cond, Or):
|
|
839
|
+
left = _box_clauses(cond.left, v, dict(env), depth + 1)
|
|
840
|
+
right = _box_clauses(cond.right, v, dict(env), depth + 1)
|
|
841
|
+
return " and ".join(left) + " or " + " and ".join(right)
|
|
842
|
+
raise AceVerbalizationError(
|
|
843
|
+
f"drs_to_ace: condition {type(cond).__name__} is not verbalizable")
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def _rewrite_neg(cond: Neg, v: _Verbalizer, env: Dict[str, _Np],
|
|
847
|
+
depth: int) -> str:
|
|
848
|
+
"""``¬∃R(C₁ ∧ … ∧ Cₙ)`` → "if C₁ … Cₙ₋₁ then it is false that Cₙ" —
|
|
849
|
+
the probed way to keep every conjunct inside the negation's scope
|
|
850
|
+
(a "that A and B" surface drops B out of the negation). The split is
|
|
851
|
+
a classical equivalence; the round trip re-checks it per DRS."""
|
|
852
|
+
box = cond.drs
|
|
853
|
+
conds = list(box.conditions)
|
|
854
|
+
back_i = None
|
|
855
|
+
for i in range(len(conds) - 1, -1, -1):
|
|
856
|
+
c = conds[i]
|
|
857
|
+
if isinstance(c, (Card, Part)):
|
|
858
|
+
continue # group infrastructure must stay with its group
|
|
859
|
+
if isinstance(c, Impl) and any(
|
|
860
|
+
_membership_duplex(c, g) is not None
|
|
861
|
+
for g in box.referents):
|
|
862
|
+
continue
|
|
863
|
+
back_i = i
|
|
864
|
+
break
|
|
865
|
+
if back_i is None:
|
|
866
|
+
raise AceVerbalizationError(
|
|
867
|
+
"drs_to_ace: negated box holds only group infrastructure — "
|
|
868
|
+
"not in the measured fragment")
|
|
869
|
+
back = conds[back_i]
|
|
870
|
+
front = conds[:back_i] + conds[back_i + 1:]
|
|
871
|
+
front_names: Set[str] = set()
|
|
872
|
+
for c in front:
|
|
873
|
+
front_names |= _cond_names(c)
|
|
874
|
+
back_only = tuple(r for r in box.referents
|
|
875
|
+
if r in _cond_names(back) and r not in front_names)
|
|
876
|
+
front_refs = tuple(r for r in box.referents if r not in back_only)
|
|
877
|
+
rewritten = Impl(DRS(front_refs, tuple(front)),
|
|
878
|
+
DRS((), (Neg(DRS(back_only, (back,))),)))
|
|
879
|
+
return _complex_clause(rewritten, v, env, depth)
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def formula_to_ace(formula) -> AceText:
|
|
883
|
+
"""Is this FORMULA expressible as ACE? Convert it — or refuse by name.
|
|
884
|
+
|
|
885
|
+
Two refusal-checked steps: :func:`unicode_logic_kit.drt.fol_to_drs`
|
|
886
|
+
rebuilds the DRS the formula is the standard translation of (raising
|
|
887
|
+
:class:`~unicode_logic_kit.drt.FolToDrsError` for anything outside that
|
|
888
|
+
image — modality, bare universals, formula-level counting, function
|
|
889
|
+
terms), then :func:`drs_to_ace` verbalizes it (raising
|
|
890
|
+
:class:`AceVerbalizationError` for DRSs outside the probed ACE
|
|
891
|
+
fragment). So the two exceptions ARE the expressibility verdict, each
|
|
892
|
+
naming what exactly does not fit. The live self-check for the result
|
|
893
|
+
is ``ace_round_trip(fol_to_drs(formula))``.
|
|
894
|
+
"""
|
|
895
|
+
from ..drt.reverse import fol_to_drs
|
|
896
|
+
|
|
897
|
+
_refuse_truth_constants(formula, "formula_to_ace")
|
|
898
|
+
return drs_to_ace(fol_to_drs(formula))
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
# ---------------------------------------------------------------------------
|
|
902
|
+
# The self-check loop
|
|
903
|
+
# ---------------------------------------------------------------------------
|
|
904
|
+
|
|
905
|
+
def ace_round_trip(drs: DRS, *, timeout: float = 30.0) -> AceRoundTrip:
|
|
906
|
+
"""DRS → ACE → APE → DRS, judged by Z3 — the machine self-check.
|
|
907
|
+
|
|
908
|
+
Needs a live APE (:func:`~unicode_logic_kit.ace.runner.ape_available`).
|
|
909
|
+
Raises :class:`AceVerbalizationError` when the DRS is outside the
|
|
910
|
+
verbalizable fragment, and propagates the mapping's own errors if the
|
|
911
|
+
produced text comes back unmappable (which would be a generator bug —
|
|
912
|
+
the tests keep that set empty over the whole mappable corpus).
|
|
913
|
+
"""
|
|
914
|
+
from ..eval.equivalence import equivalent
|
|
915
|
+
|
|
916
|
+
verbal = drs_to_ace(drs)
|
|
917
|
+
back = ace_to_drs(verbal.text, ulex=verbal.ulex or None, timeout=timeout)
|
|
918
|
+
ours = drs_to_fol(drs)
|
|
919
|
+
theirs = drs_to_fol(back)
|
|
920
|
+
result = equivalent(ours, theirs)
|
|
921
|
+
detail = "" if result.equivalent else (
|
|
922
|
+
f"forward: {ours.to_unicode_str()} back: {theirs.to_unicode_str()}")
|
|
923
|
+
return AceRoundTrip(verbalization=verbal, back=back,
|
|
924
|
+
equivalent=bool(result.equivalent), detail=detail)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
# ---------------------------------------------------------------------------
|
|
928
|
+
# ACE-7: modal/deontic formulas → ACE text
|
|
929
|
+
# ---------------------------------------------------------------------------
|
|
930
|
+
|
|
931
|
+
def _modal_referents_shell(modal) -> DRS:
|
|
932
|
+
"""A referents-only synthetic DRS, purely to seed :class:`_Verbalizer`'s
|
|
933
|
+
declared-referent set the same way :func:`drs_to_ace` does — the modal
|
|
934
|
+
wrapper itself is not a DRS (see :mod:`unicode_logic_kit.ace.reverse_modal`),
|
|
935
|
+
but every referent it carries was already checked for global uniqueness
|
|
936
|
+
by the ``fol_to_drs`` call(s) that built it, so combining them here is
|
|
937
|
+
safe."""
|
|
938
|
+
if isinstance(modal, ModalBox):
|
|
939
|
+
refs = _all_referents(modal.drs)
|
|
940
|
+
else:
|
|
941
|
+
assert isinstance(modal, ModalImpl)
|
|
942
|
+
refs = _all_referents(modal.antecedent) | _all_referents(modal.consequent)
|
|
943
|
+
return DRS(tuple(sorted(refs)), ())
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def _one_clause(box: DRS, clauses: List[str], where: str) -> str:
|
|
947
|
+
"""The only shape measured live under a modal auxiliary: the box's sole
|
|
948
|
+
clause must be the EVENT-ANCHORED verb clause the modality actually
|
|
949
|
+
attaches to. ``modality`` only ever reaches the text through
|
|
950
|
+
:func:`verb_clause`'s own words list (see ``_box_clauses``'s docstring)
|
|
951
|
+
— a box whose single clause instead comes from the "there is a NOUN"
|
|
952
|
+
introduction, the predicative-constant path ("X is a NOUN"), a bare
|
|
953
|
+
equality or a comparative would pass a naive ``len(clauses) == 1`` count
|
|
954
|
+
while silently DROPPING the modal auxiliary from the text (found live:
|
|
955
|
+
``□Man(john)`` -> "John is a man.", no "must"). Since ``verb_clause`` is
|
|
956
|
+
reached only through a box's OWN event referent (assembly step 3 in
|
|
957
|
+
``_box_clauses``), requiring that referent set to be exactly one event
|
|
958
|
+
is equivalent to requiring the single clause be that verb clause."""
|
|
959
|
+
single_verb_clause = (len(clauses) == 1 and len(box.referents) == 1
|
|
960
|
+
and _is_event(box.referents[0]))
|
|
961
|
+
if not single_verb_clause:
|
|
962
|
+
raise AceVerbalizationError(
|
|
963
|
+
f"drs_to_ace: a modal box with {len(clauses)} clause(s) {where} "
|
|
964
|
+
"is outside the probed ACE fragment — only a single verb clause "
|
|
965
|
+
"has been measured under a modal auxiliary "
|
|
966
|
+
"('must'/'can'/'should'/'may')")
|
|
967
|
+
return clauses[0]
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def modal_drs_to_ace(modal) -> AceText:
|
|
971
|
+
"""Verbalize a modal/deontic DRS shell as ACE text (ACE-7).
|
|
972
|
+
|
|
973
|
+
``modal`` is whatever :func:`~unicode_logic_kit.ace.reverse_modal.fol_to_modal_drs`
|
|
974
|
+
returns: a plain classical :class:`~unicode_logic_kit.drt.nodes.DRS`
|
|
975
|
+
(delegated to :func:`drs_to_ace` unchanged), a
|
|
976
|
+
:class:`~unicode_logic_kit.ace.reverse_modal.ModalBox` ("John must
|
|
977
|
+
wait."), or a :class:`~unicode_logic_kit.ace.reverse_modal.ModalImpl`
|
|
978
|
+
("If there is a man X1 then X1 must wait."). Both modal shapes are
|
|
979
|
+
restricted to a box that renders to EXACTLY ONE clause — the only shape
|
|
980
|
+
measured live under a modal auxiliary; anything else raises
|
|
981
|
+
:class:`AceVerbalizationError` naming the construct, same discipline as
|
|
982
|
+
:func:`drs_to_ace`.
|
|
983
|
+
"""
|
|
984
|
+
if isinstance(modal, DRS):
|
|
985
|
+
return drs_to_ace(modal)
|
|
986
|
+
if isinstance(modal, ModalBox):
|
|
987
|
+
v = _Verbalizer(_modal_referents_shell(modal))
|
|
988
|
+
clause = _one_clause(
|
|
989
|
+
modal.drs,
|
|
990
|
+
_box_clauses(modal.drs, v, {}, depth=0, modality=modal.modality),
|
|
991
|
+
"under its modality")
|
|
992
|
+
sentence = clause[0].upper() + clause[1:] + "."
|
|
993
|
+
return AceText(text=sentence, ulex=v.lexicon.text())
|
|
994
|
+
if isinstance(modal, ModalImpl):
|
|
995
|
+
v = _Verbalizer(_modal_referents_shell(modal))
|
|
996
|
+
shared: Dict[str, _Np] = {}
|
|
997
|
+
ante = _box_clauses(modal.antecedent, v, shared, depth=1)
|
|
998
|
+
cons = _one_clause(
|
|
999
|
+
modal.consequent,
|
|
1000
|
+
_box_clauses(modal.consequent, v, shared, depth=1,
|
|
1001
|
+
modality=modal.modality),
|
|
1002
|
+
"in a duplex consequent")
|
|
1003
|
+
sentence = ("if " + " and ".join(ante) + " then " + cons)
|
|
1004
|
+
sentence = sentence[0].upper() + sentence[1:] + "."
|
|
1005
|
+
return AceText(text=sentence, ulex=v.lexicon.text())
|
|
1006
|
+
raise TypeError(
|
|
1007
|
+
f"modal_drs_to_ace: expected a DRS/ModalBox/ModalImpl, got "
|
|
1008
|
+
f"{type(modal).__name__}")
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
def modal_formula_to_ace(formula) -> AceText:
|
|
1012
|
+
"""Is this modal/deontic FORMULA expressible as ACE? Convert it — or
|
|
1013
|
+
refuse by name (ACE-7's :func:`formula_to_ace` counterpart).
|
|
1014
|
+
|
|
1015
|
+
Two refusal-checked steps, mirroring :func:`formula_to_ace` exactly:
|
|
1016
|
+
:func:`~unicode_logic_kit.ace.reverse_modal.fol_to_modal_drs` rebuilds the
|
|
1017
|
+
DRS (or modal DRS shell) the formula is the standard translation of
|
|
1018
|
+
(raising :class:`~unicode_logic_kit.drt.reverse.FolToDrsError` for
|
|
1019
|
+
anything outside that image — including a modal placement neither of
|
|
1020
|
+
the two probed shapes covers), then :func:`modal_drs_to_ace` verbalizes
|
|
1021
|
+
it (raising :class:`AceVerbalizationError` for a modal box outside the
|
|
1022
|
+
single-clause fragment). The live self-check is
|
|
1023
|
+
:func:`modal_ace_round_trip`.
|
|
1024
|
+
"""
|
|
1025
|
+
from .reverse_modal import fol_to_modal_drs
|
|
1026
|
+
|
|
1027
|
+
_refuse_truth_constants(formula, "modal_formula_to_ace")
|
|
1028
|
+
return modal_drs_to_ace(fol_to_modal_drs(formula))
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
@dataclass(frozen=True)
|
|
1032
|
+
class ModalAceRoundTrip:
|
|
1033
|
+
"""One closed (or failed) loop for the modal route: formula → ACE →
|
|
1034
|
+
APE → formula, judged by :func:`~unicode_logic_kit.eval.equivalence.equivalent`
|
|
1035
|
+
(NOT raw Z3 directly — a modal ``Node``'s own ``to_z3`` refuses by
|
|
1036
|
+
design; ``equivalent`` already routes a modal pair through
|
|
1037
|
+
``modal_decide``/``qml_equivalent``, see that module)."""
|
|
1038
|
+
|
|
1039
|
+
verbalization: AceText
|
|
1040
|
+
back: "unicode_logic_kit.ace.AceFormula"
|
|
1041
|
+
equivalent: bool
|
|
1042
|
+
detail: str = ""
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def modal_ace_round_trip(formula, *, timeout: float = 30.0) -> ModalAceRoundTrip:
|
|
1046
|
+
"""Formula → ACE → APE → formula, judged by
|
|
1047
|
+
:func:`~unicode_logic_kit.eval.equivalence.equivalent` — the modal
|
|
1048
|
+
counterpart of :func:`ace_round_trip`.
|
|
1049
|
+
|
|
1050
|
+
Needs a live APE (:func:`~unicode_logic_kit.ace.runner.ape_available`).
|
|
1051
|
+
Raises :class:`~unicode_logic_kit.drt.reverse.FolToDrsError` or
|
|
1052
|
+
:class:`AceVerbalizationError` when ``formula`` is outside the
|
|
1053
|
+
verbalizable fragment (see :func:`modal_formula_to_ace`), and
|
|
1054
|
+
propagates :func:`~unicode_logic_kit.ace.translate.ace_to_formula`'s own
|
|
1055
|
+
errors if the produced text comes back unmappable on the backward leg
|
|
1056
|
+
(a generator bug — the tests keep that set empty over the probed
|
|
1057
|
+
corpus).
|
|
1058
|
+
"""
|
|
1059
|
+
from ..eval.equivalence import equivalent
|
|
1060
|
+
from .translate import ace_to_formula
|
|
1061
|
+
|
|
1062
|
+
verbal = modal_formula_to_ace(formula)
|
|
1063
|
+
back = ace_to_formula(verbal.text, ulex=verbal.ulex or None,
|
|
1064
|
+
timeout=timeout)
|
|
1065
|
+
result = equivalent(formula, back.formula)
|
|
1066
|
+
detail = "" if result.equivalent else (
|
|
1067
|
+
f"forward: {formula.to_unicode_str()} "
|
|
1068
|
+
f"back: {back.formula.to_unicode_str()}")
|
|
1069
|
+
return ModalAceRoundTrip(verbalization=verbal, back=back,
|
|
1070
|
+
equivalent=bool(result.equivalent), detail=detail)
|