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,510 @@
|
|
|
1
|
+
"""The key of an atom on a route that identifies an atom by its written form.
|
|
2
|
+
|
|
3
|
+
A propositional route (a truth table, a many-valued or fuzzy evaluator, the intuitionistic
|
|
4
|
+
and counterfactual deciders, a trace of a linear-time formula) takes every ground atom as a
|
|
5
|
+
letter of its own and names the letter by the KEY of the atom: the text it prints as, with
|
|
6
|
+
every constant written by its bare name (``key_text`` of ``fol/_msfl_nodes.py``:
|
|
7
|
+
``'P'``, ``'P(a)'``, ``'Likes(a, b)'``). The text of the FORMULA (``Atom.to_unicode_str()``)
|
|
8
|
+
writes a constant in quotes when its bare name would read as something else (``P('a')``);
|
|
9
|
+
the key does not, so a valuation typed as ``{"P(a)": True}`` keeps meaning the atom ``P``
|
|
10
|
+
over the element ``a``, as it always did.
|
|
11
|
+
|
|
12
|
+
An atom whose constants are all bare-readable has ONE spelling: its key and its formula text
|
|
13
|
+
are the same string. An atom with a quoted constant has TWO, and a caller's valuation may be
|
|
14
|
+
keyed in either: the guide taught ``atom.to_unicode_str()`` as "the key", and a hand-built atom
|
|
15
|
+
over ``Constant("a")`` prints ``Likes('a', 'b')`` since quoted constants exist. So a lookup in a
|
|
16
|
+
table that comes FROM THE CALLER goes through :func:`find_key`, which reads the key and then the
|
|
17
|
+
formula text; a table the kit builds itself and reads back is keyed by :func:`atom_key` alone.
|
|
18
|
+
The two spellings cannot name two atoms because a key that holds a complete quoted constant
|
|
19
|
+
(that of ``Constant("'a'")``: its key ``P('a')`` is also the formula text of ``P`` over the
|
|
20
|
+
constant ``a``) is refused where a key is made.
|
|
21
|
+
|
|
22
|
+
The key is faithful only while two different atoms never have one. Two do, and the route would
|
|
23
|
+
read them as ONE letter and answer about another problem:
|
|
24
|
+
|
|
25
|
+
* the numeral ``1`` and a constant named ``1`` (``Number(1)`` and ``Constant('1')``: the
|
|
26
|
+
TPTP reader makes the pair from ``p(1)`` and ``p('1')``): ``P(1) ⊢ P('1')`` would be valid;
|
|
27
|
+
* a free variable ``x`` and a constant named ``x``: ``P(c) → P(x)`` would be valid for
|
|
28
|
+
``c = Constant('x')``, although a free variable is a parameter (one unknown element), not
|
|
29
|
+
the constant of its name;
|
|
30
|
+
* a constant named like a compound term (``Constant('f(a)')`` and ``f(a)``).
|
|
31
|
+
|
|
32
|
+
:class:`AtomKeys` is the one place the key is built. It gives every atom of one problem its
|
|
33
|
+
key and refuses, by name, a problem in which two different atoms would get the same key (the
|
|
34
|
+
refusal every route that names a symbol by its text already gives for a numeral and a constant
|
|
35
|
+
of one spelling). :func:`atom_key` is the key alone, for a route that already knows the
|
|
36
|
+
problem has no such pair.
|
|
37
|
+
|
|
38
|
+
A sorted constant ``c:S`` is the constant ``c`` (it denotes an element of ``S``): its key is
|
|
39
|
+
the key of the plain constant, so ``Mortal(c:S)`` and ``Mortal(c)`` are ONE letter. A route
|
|
40
|
+
that cannot state what membership of the sort means for its truth values refuses the sorted
|
|
41
|
+
constant by name instead (``sorted_constants="refuse"``).
|
|
42
|
+
|
|
43
|
+
The operators of an agent are named in the same way: a model keeps the relation of ``K_a`` under
|
|
44
|
+
the name ``"K:" + agent_key(a)``, so two different agent terms with one key (the numeral ``1``
|
|
45
|
+
and the constant ``'1'``) would be ONE agent. :func:`refuse_alike_agents` refuses such a pair,
|
|
46
|
+
for the routes that file a relation, a tableau branch or a first-order predicate under that name.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
import re
|
|
50
|
+
from collections.abc import Mapping
|
|
51
|
+
from functools import lru_cache
|
|
52
|
+
from typing import Any, Container, Dict, Iterable, List, Optional, Tuple, Type
|
|
53
|
+
|
|
54
|
+
from ._fol_nodes import Atom, Constant, Function, Node, Number, Variable
|
|
55
|
+
from ._identifiers import (
|
|
56
|
+
_UNSPELLABLE, _unquote_constant, is_bare_constant, quoted_name_pattern)
|
|
57
|
+
from ._modal_nodes import (
|
|
58
|
+
Believes, CommonKnowledge, DistributedKnowledge, EverybodyKnows, Knows, Says, Wants)
|
|
59
|
+
from ._msfl_nodes import SortedConstant, key_text
|
|
60
|
+
from ._truth_constants import truth_value
|
|
61
|
+
|
|
62
|
+
__all__ = ["atom_key", "other_key", "find_key", "find_own_key", "plain_atom",
|
|
63
|
+
"refuse_sorted_constant", "AtomKeys", "agent_key", "refuse_alike_agents"]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@lru_cache(maxsize=1)
|
|
67
|
+
def _free_standing_quoted_token():
|
|
68
|
+
"""A complete QUOTED_NAME token with no letter, digit or apostrophe next to it.
|
|
69
|
+
|
|
70
|
+
Where the text of a formula writes a constant in quotes, the token stands free: what
|
|
71
|
+
comes before it is the start, ``(`` or a space, and what follows is the end, ``)``, ``,``, a
|
|
72
|
+
space, ``:`` or ``}``. The pattern asks for less (anything but a letter, a digit or an
|
|
73
|
+
apostrophe on either side), so it finds every such token and some text that is none.
|
|
74
|
+
"""
|
|
75
|
+
return re.compile(r"(?<![^\W_])(?<!')" + quoted_name_pattern() + r"(?![^\W_])(?!')")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@lru_cache(maxsize=8192)
|
|
79
|
+
def _has_text(name: str) -> bool:
|
|
80
|
+
"""Whether a constant of this (not bare-readable) name has a text, in quotes.
|
|
81
|
+
|
|
82
|
+
The empty name and a name with a control character have none, so the formula text of an atom
|
|
83
|
+
that holds such a constant does not exist.
|
|
84
|
+
"""
|
|
85
|
+
return name != "" and _UNSPELLABLE.search(name) is None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _refuse_key_that_reads_as_text(key: str, route: str, error: Type[Exception]) -> None:
|
|
89
|
+
"""Refuse a key that holds a complete quoted constant.
|
|
90
|
+
|
|
91
|
+
A key writes every name as it is, so a quote in a key is part of a NAME (a constant named
|
|
92
|
+
``'a'``, one named ``ab, 'b'``, a proposition a TPTP file calls ``'p(\\'a\\')'``). The text
|
|
93
|
+
of a formula writes a quote only around a constant. A key that holds a whole quoted
|
|
94
|
+
constant, standing free, may therefore be the text of ANOTHER atom as a formula: ``P('a')``
|
|
95
|
+
is the key of ``P`` over the constant named ``'a'`` and the text of ``P`` over the constant
|
|
96
|
+
``a``. A valuation may be keyed either way, so one entry would answer for two atoms.
|
|
97
|
+
|
|
98
|
+
The check is on the safe side. It refuses every key that is the formula text of another atom
|
|
99
|
+
with a quoted constant, and also a key such as ``P(rock 'n' roll)``, which is the text of no
|
|
100
|
+
formula. A name with one apostrophe (``D'Alembert``), with apostrophes next to letters or
|
|
101
|
+
digits (``3',5'-cyclic``), or two such names in one atom, is not refused.
|
|
102
|
+
"""
|
|
103
|
+
found = _free_standing_quoted_token().search(key)
|
|
104
|
+
if found is None:
|
|
105
|
+
return
|
|
106
|
+
token = found.group(0)
|
|
107
|
+
inner = _unquote_constant(token)
|
|
108
|
+
raise error(
|
|
109
|
+
f"{route}: the key of this atom, {key!r}, reads as the text of another atom. A key "
|
|
110
|
+
f"writes every name as it is, and a name here holds apostrophes, so the key holds "
|
|
111
|
+
f"{token}, which is how a formula writes the constant named \"{inner}\" (in quotes). "
|
|
112
|
+
f"A valuation may be keyed by the key of an atom or by its text as a formula, so one "
|
|
113
|
+
f"entry would answer for two atoms. Rename the symbol (a name that merely holds an "
|
|
114
|
+
f"apostrophe, such as D'Alembert, is fine).")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _survey(atom: Node) -> Tuple[bool, bool, bool]:
|
|
118
|
+
"""One walk over ``atom``: ``(has a sorted constant, its formula text writes a quote, a name
|
|
119
|
+
of it may hold an apostrophe)``.
|
|
120
|
+
|
|
121
|
+
The third is what :func:`_refuse_key_that_reads_as_text` is asked about: a key holds a quote
|
|
122
|
+
only where a name does, so an atom whose names hold none is never scanned.
|
|
123
|
+
"""
|
|
124
|
+
if type(atom) is Atom and not atom.args:
|
|
125
|
+
return False, False, "'" in atom.predicate # a proposition holds no constant
|
|
126
|
+
has_sorted = False
|
|
127
|
+
has_quoted = False
|
|
128
|
+
has_no_text = False
|
|
129
|
+
apostrophe = False
|
|
130
|
+
# The same nodes as ``atom.walk()``, but without the generic ``_child_nodes`` (which reads
|
|
131
|
+
# the dataclass fields of every node): a key is made for every atom of every evaluation,
|
|
132
|
+
# and an atom is a handful of constants, variables and numerals. A node of another kind
|
|
133
|
+
# takes the generic way.
|
|
134
|
+
# ``Any``: the node kinds are told apart by their class below, which a type checker does
|
|
135
|
+
# not follow, and each branch reads the fields of the class it found.
|
|
136
|
+
stack: List[Any] = [atom]
|
|
137
|
+
while stack:
|
|
138
|
+
node = stack.pop()
|
|
139
|
+
kind = type(node)
|
|
140
|
+
if kind is Number:
|
|
141
|
+
continue
|
|
142
|
+
if kind is Variable:
|
|
143
|
+
apostrophe = apostrophe or "'" in node.name
|
|
144
|
+
continue
|
|
145
|
+
if kind is Atom:
|
|
146
|
+
apostrophe = apostrophe or "'" in node.predicate
|
|
147
|
+
stack.extend(node.args)
|
|
148
|
+
continue
|
|
149
|
+
if kind is Function:
|
|
150
|
+
apostrophe = apostrophe or "'" in node.name
|
|
151
|
+
stack.extend(node.args)
|
|
152
|
+
continue
|
|
153
|
+
if kind is SortedConstant:
|
|
154
|
+
has_sorted = True
|
|
155
|
+
elif kind is not Constant:
|
|
156
|
+
stack.extend(node._child_nodes())
|
|
157
|
+
if not isinstance(node, (Constant, SortedConstant)):
|
|
158
|
+
# a node of a kind not listed here: its own name is not looked at, so the key
|
|
159
|
+
# is scanned to be sure
|
|
160
|
+
apostrophe = True
|
|
161
|
+
continue
|
|
162
|
+
has_sorted = has_sorted or isinstance(node, SortedConstant)
|
|
163
|
+
name = node.name
|
|
164
|
+
if not isinstance(name, str):
|
|
165
|
+
has_no_text = True
|
|
166
|
+
elif is_bare_constant(name):
|
|
167
|
+
continue # a bare name holds no apostrophe
|
|
168
|
+
else:
|
|
169
|
+
apostrophe = apostrophe or "'" in name
|
|
170
|
+
if _has_text(name):
|
|
171
|
+
has_quoted = True
|
|
172
|
+
else:
|
|
173
|
+
has_no_text = True
|
|
174
|
+
# an atom that holds a constant without a text has no formula text at all
|
|
175
|
+
return has_sorted, has_quoted and not has_no_text, apostrophe
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _key(atom: Node, route: str, error: Type[Exception]) -> Tuple[str, Node, bool]:
|
|
179
|
+
"""``(key, plain atom, whether its formula text differs from the key)`` of ``atom``.
|
|
180
|
+
|
|
181
|
+
The one place a key is rendered. A key that reads as the formula text of another atom is
|
|
182
|
+
refused here (:func:`_refuse_key_that_reads_as_text`).
|
|
183
|
+
"""
|
|
184
|
+
has_sorted, quoted, apostrophe = _survey(atom)
|
|
185
|
+
plain = _plain(atom) if has_sorted else atom
|
|
186
|
+
key = key_text(plain)
|
|
187
|
+
if apostrophe:
|
|
188
|
+
_refuse_key_that_reads_as_text(key, route, error)
|
|
189
|
+
return key, plain, quoted
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _has_sorted_constant(node: Node) -> bool:
|
|
193
|
+
"""Whether a sorted constant occurs anywhere inside ``node``."""
|
|
194
|
+
return any(isinstance(n, SortedConstant) for n in node.walk())
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _plain(node: Node) -> Node:
|
|
198
|
+
"""``node`` with every sorted constant ``c:S`` replaced by the plain constant ``c``."""
|
|
199
|
+
if isinstance(node, SortedConstant):
|
|
200
|
+
return Constant(node.name)
|
|
201
|
+
return node.map_children(_plain)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def plain_atom(atom: Node) -> Node:
|
|
205
|
+
"""The atom with each sorted constant ``c:S`` read as the constant ``c`` (the atom itself
|
|
206
|
+
when it has none)."""
|
|
207
|
+
return _plain(atom) if _has_sorted_constant(atom) else atom
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def atom_key(atom: Node) -> str:
|
|
211
|
+
"""The key of an atom: the text of the atom with every constant written by its bare name.
|
|
212
|
+
|
|
213
|
+
This is the definition of a key. The valuations, models and traces that the evaluators of
|
|
214
|
+
the kit read (``satisfies_modal``, ``fuzzy_evaluate``, the many-valued, matrix,
|
|
215
|
+
intuitionistic and counterfactual evaluators, ``ltl_trace_satisfies``, the probabilistic
|
|
216
|
+
programs) are keyed by it, and so is every table the kit RETURNS (the countermodels of the
|
|
217
|
+
modal, intuitionistic and counterfactual deciders, the models of a tableau, the columns of a
|
|
218
|
+
truth table). A sorted constant ``c:S`` is written ``c``, as the constant it is.
|
|
219
|
+
|
|
220
|
+
The key is NOT the text of the formula. ``Constant("k2")`` is written ``'k2'`` in the formula
|
|
221
|
+
text, so that the text reads back as that constant, and ``k2`` in the key: for the atom
|
|
222
|
+
``Likes`` over the constants ``a`` and ``b`` the key is ``Likes(a, b)`` and the text of the
|
|
223
|
+
formula is ``Likes('a', 'b')``. For an atom whose constants are all bare-readable
|
|
224
|
+
(``Likes(alice, bob)``) the two are the same string. The evaluators also read a valuation
|
|
225
|
+
that a caller keyed by the text of the atom as a formula, ``atom.to_unicode_str()``, so
|
|
226
|
+
either spelling is found.
|
|
227
|
+
|
|
228
|
+
No check is made that another atom does not have the same key; :class:`AtomKeys` does that
|
|
229
|
+
for the atoms of one problem.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
NotImplementedError: the key would hold a complete quoted constant, because a name of
|
|
233
|
+
the atom holds apostrophes (``Constant("'a'")``, ``Constant("ab, 'b'")``): such a key
|
|
234
|
+
is also the formula text of another atom (``P`` over the constant ``a``), and one
|
|
235
|
+
key must not name two atoms. A name that merely holds an apostrophe
|
|
236
|
+
(``D'Alembert``, ``3',5'-cyclic``) has its key.
|
|
237
|
+
"""
|
|
238
|
+
return _key(atom, "atom_key", NotImplementedError)[0]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def other_key(atom: Node) -> Optional[str]:
|
|
242
|
+
"""The OTHER spelling of the key of an atom: its text as a formula, when that differs.
|
|
243
|
+
|
|
244
|
+
``None`` when no constant of the atom is written in quotes (every constant reads back
|
|
245
|
+
bare), where the formula text and :func:`atom_key` are one string and nothing is rendered; else
|
|
246
|
+
the text of the same plain atom as a formula (``Likes('a', 'b')`` for the key
|
|
247
|
+
``Likes(a, b)``). A valuation a caller keyed by ``atom.to_unicode_str()`` holds this one.
|
|
248
|
+
The two spellings are different strings whenever the second exists, and the refusal of a
|
|
249
|
+
key that holds a quoted constant (:func:`atom_key`) keeps them from naming two atoms.
|
|
250
|
+
|
|
251
|
+
Raises:
|
|
252
|
+
NotImplementedError: as :func:`atom_key`.
|
|
253
|
+
"""
|
|
254
|
+
has_sorted, quoted, apostrophe = _survey(atom)
|
|
255
|
+
plain = _plain(atom) if has_sorted else atom
|
|
256
|
+
if apostrophe:
|
|
257
|
+
_refuse_key_that_reads_as_text(key_text(plain), "other_key", NotImplementedError)
|
|
258
|
+
return plain.to_unicode_str() if quoted else None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _locate(table: Container[str], key: str, plain: Node, quoted: bool,
|
|
262
|
+
route: str) -> Optional[str]:
|
|
263
|
+
"""The body of :func:`find_key`, for an atom whose ``key`` is made already."""
|
|
264
|
+
if not quoted:
|
|
265
|
+
return key if key in table else None
|
|
266
|
+
if key in table:
|
|
267
|
+
if isinstance(table, Mapping):
|
|
268
|
+
other = plain.to_unicode_str()
|
|
269
|
+
if other in table:
|
|
270
|
+
a, b = table[key], table[other]
|
|
271
|
+
if not (a is b or a == b):
|
|
272
|
+
raise ValueError(
|
|
273
|
+
f"{route}: one atom, two entries. The atom {other} is keyed both "
|
|
274
|
+
f"{key!r} (value {a!r}) and {other!r} (value {b!r}) in this table, "
|
|
275
|
+
f"and the two values differ. A key written with the bare names of the "
|
|
276
|
+
f"constants and one written as the text of the formula are two "
|
|
277
|
+
f"spellings of ONE atom, and the table says two different things "
|
|
278
|
+
f"about it. Keep one of the two entries.")
|
|
279
|
+
return key
|
|
280
|
+
other = plain.to_unicode_str()
|
|
281
|
+
return other if other in table else None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def find_own_key(table: Container[str], atom: Node) -> Optional[str]:
|
|
285
|
+
"""The key under which a table THE KIT BUILT holds ``atom``, or ``None``.
|
|
286
|
+
|
|
287
|
+
For a route that fills a table itself, keyed by :func:`atom_key`, and reads it back (the rows
|
|
288
|
+
of a truth table, the assignments an enumeration tries): there is no second spelling to look
|
|
289
|
+
for, and the lookup is as cheap as it was before the formula text of a constant was quoted.
|
|
290
|
+
A table that comes from a caller is read by :func:`find_key`.
|
|
291
|
+
"""
|
|
292
|
+
key = atom_key(atom)
|
|
293
|
+
return key if key in table else None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def find_key(keys: Container[str], atom: Node) -> Optional[str]:
|
|
297
|
+
"""The key under which ``keys`` holds ``atom``, or ``None``.
|
|
298
|
+
|
|
299
|
+
``keys`` is what a caller's table offers for ``in``: the set of keys true at one world, a
|
|
300
|
+
dict from key to value. The atom is looked up under :func:`atom_key` first (the bare names of
|
|
301
|
+
its constants, ``Likes(a, b)``) and then under its text as a formula
|
|
302
|
+
(``Likes('a', 'b')``), the key a caller gets from ``atom.to_unicode_str()``. An atom whose two
|
|
303
|
+
spellings are one string is looked up once and nothing is rendered a second time.
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
ValueError: ``keys`` is a mapping that holds both spellings of the atom with different
|
|
307
|
+
values (one atom, two entries); equal values, or both spellings in a set, are no
|
|
308
|
+
conflict.
|
|
309
|
+
NotImplementedError: as :func:`atom_key`.
|
|
310
|
+
"""
|
|
311
|
+
key, plain, quoted = _key(atom, "find_key", NotImplementedError)
|
|
312
|
+
return _locate(keys, key, plain, quoted, "find_key")
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def refuse_sorted_constant(atom: Node, route: str,
|
|
316
|
+
error: Type[Exception] = NotImplementedError) -> None:
|
|
317
|
+
"""Refuse an atom that holds a sorted constant, by name.
|
|
318
|
+
|
|
319
|
+
For a route that has no reading of what ``c:S`` being an element of ``S`` means for its
|
|
320
|
+
values: reading ``S(c)`` as unrelated to the annotation would answer about another
|
|
321
|
+
problem. ``error`` is the exception class raised (the exception a route already uses for
|
|
322
|
+
the input it refuses).
|
|
323
|
+
|
|
324
|
+
Raises:
|
|
325
|
+
NotImplementedError: a sorted constant occurs in ``atom`` (or the class ``error``).
|
|
326
|
+
"""
|
|
327
|
+
for sub in atom.walk():
|
|
328
|
+
if isinstance(sub, SortedConstant):
|
|
329
|
+
raise error(
|
|
330
|
+
f"{route}: the sorted constant {sub.to_unicode_str()} in the atom "
|
|
331
|
+
f"{atom.to_unicode_str()!r} has no reading here. {sub.name}:{sub.sort} is the "
|
|
332
|
+
f"constant {sub.name} and lies in the sort {sub.sort}, a fact about the sort "
|
|
333
|
+
f"that this route has no way to state for its values, and reading the atom as "
|
|
334
|
+
f"another letter would answer about another problem. Write the constant "
|
|
335
|
+
f"without its sort, and the fact as an atom ({sub.sort}({sub.name})) if it "
|
|
336
|
+
f"matters.")
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _first_difference(a: Node, b: Node) -> Tuple[Node, Node]:
|
|
340
|
+
"""The first pair of corresponding sub-terms of two atoms that are not equal."""
|
|
341
|
+
if isinstance(a, Atom) and isinstance(b, Atom):
|
|
342
|
+
same_head = a.predicate == b.predicate
|
|
343
|
+
a_args, b_args = a.args, b.args
|
|
344
|
+
elif isinstance(a, Function) and isinstance(b, Function):
|
|
345
|
+
same_head = a.name == b.name
|
|
346
|
+
a_args, b_args = a.args, b.args
|
|
347
|
+
else:
|
|
348
|
+
return a, b
|
|
349
|
+
if same_head and len(a_args) == len(b_args):
|
|
350
|
+
for x, y in zip(a_args, b_args):
|
|
351
|
+
if x != y:
|
|
352
|
+
return _first_difference(x, y)
|
|
353
|
+
return a, b
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _why_alike(a: Node, b: Node) -> str:
|
|
357
|
+
"""One sentence on why two different terms are written alike."""
|
|
358
|
+
for numeral, constant in ((a, b), (b, a)):
|
|
359
|
+
if isinstance(numeral, Number) and isinstance(constant, Constant):
|
|
360
|
+
return (f"The numeral {numeral.to_unicode_str()} and a constant named "
|
|
361
|
+
f"{constant.name!r} are one symbol on such a route (a numeral is the "
|
|
362
|
+
f"constant of its value, written as its digits). Write the number as a "
|
|
363
|
+
f"constant of another name, or rename the constant.")
|
|
364
|
+
for variable, constant in ((a, b), (b, a)):
|
|
365
|
+
if isinstance(variable, Variable) and isinstance(constant, Constant):
|
|
366
|
+
return (f"A free variable is a parameter, one unknown element of the problem, and "
|
|
367
|
+
f"not the constant named {variable.name!r}; a symbol named by its text "
|
|
368
|
+
f"cannot tell them apart. Rename the constant or the variable.")
|
|
369
|
+
return ("They are different terms with one written form, which a letter named by text "
|
|
370
|
+
"cannot tell apart. Rename one of the symbols.")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class AtomKeys:
|
|
374
|
+
"""The keys of the atoms of ONE problem, refusing two different atoms with one key.
|
|
375
|
+
|
|
376
|
+
Create one per problem and pass every atom of it through :meth:`key`; the first atom
|
|
377
|
+
that has the key of a different atom ends in a ``NotImplementedError`` that names both.
|
|
378
|
+
Two atoms are the same atom when their nodes are equal after each sorted constant has
|
|
379
|
+
been read as the plain constant (``Number(1)`` and ``Number(1.0)`` are one node). A route
|
|
380
|
+
that reads a valuation from its caller passes each atom through :meth:`find` instead, which
|
|
381
|
+
does the same and then looks the atom up in the caller's table under either spelling.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
route: what to call the route in a refusal (``'is_tautology'``, ``'int_prove'``).
|
|
385
|
+
sorted_constants: ``"read"`` (the default) keys ``c:S`` as the constant ``c``;
|
|
386
|
+
``"refuse"`` refuses a sorted constant by name, for a route that has no reading of
|
|
387
|
+
what membership of a sort means for its values.
|
|
388
|
+
error: the exception class a refusal raises, ``NotImplementedError`` by default; a
|
|
389
|
+
route whose callers already handle another class for the input it refuses
|
|
390
|
+
(``ValueError``) names it here.
|
|
391
|
+
"""
|
|
392
|
+
|
|
393
|
+
def __init__(self, route: str, sorted_constants: str = "read",
|
|
394
|
+
error: Type[Exception] = NotImplementedError) -> None:
|
|
395
|
+
if sorted_constants not in ("read", "refuse"):
|
|
396
|
+
raise ValueError(f"AtomKeys: sorted_constants must be 'read' or 'refuse', "
|
|
397
|
+
f"got {sorted_constants!r}.")
|
|
398
|
+
self._route = route
|
|
399
|
+
self._refuse_sorted = sorted_constants == "refuse"
|
|
400
|
+
self._error = error
|
|
401
|
+
self._atoms: Dict[str, Node] = {}
|
|
402
|
+
|
|
403
|
+
def key(self, atom: Node) -> str:
|
|
404
|
+
"""The key of ``atom``, which is also recorded as the atom that has it.
|
|
405
|
+
|
|
406
|
+
Raises:
|
|
407
|
+
NotImplementedError: the atom holds a sorted constant on a route that refuses
|
|
408
|
+
it, the key would hold a complete quoted constant (that of
|
|
409
|
+
``Constant("'a'")`` is also the formula text of the atom over the constant
|
|
410
|
+
``a``), or a different atom already has the same key (the class ``error`` of
|
|
411
|
+
the constructor).
|
|
412
|
+
"""
|
|
413
|
+
return self._record(atom)[0]
|
|
414
|
+
|
|
415
|
+
def find(self, table: Container[str], atom: Node) -> Optional[str]:
|
|
416
|
+
"""The key under which a caller's ``table`` holds ``atom``, or ``None``.
|
|
417
|
+
|
|
418
|
+
What :func:`find_key` does, after what :meth:`key` does: the atom is recorded and
|
|
419
|
+
checked against the other atoms of the problem first, so the refusal of two different
|
|
420
|
+
atoms with one key keeps working where a route reads a caller's valuation. ``table`` is
|
|
421
|
+
what the caller's valuation offers for ``in`` (a set of keys, a dict from key to value).
|
|
422
|
+
|
|
423
|
+
Raises:
|
|
424
|
+
ValueError: ``table`` is a mapping that holds both spellings of the atom (its key
|
|
425
|
+
and its text as a formula) with different values.
|
|
426
|
+
NotImplementedError: as :meth:`key`.
|
|
427
|
+
"""
|
|
428
|
+
key, plain, quoted = self._record(atom)
|
|
429
|
+
return _locate(table, key, plain, quoted, self._route)
|
|
430
|
+
|
|
431
|
+
def _record(self, atom: Node) -> Tuple[str, Node, bool]:
|
|
432
|
+
"""``(key, plain atom, whether its formula text differs)`` of ``atom``, recorded."""
|
|
433
|
+
if self._refuse_sorted:
|
|
434
|
+
refuse_sorted_constant(atom, self._route, self._error)
|
|
435
|
+
key, plain, quoted = _key(atom, self._route, self._error)
|
|
436
|
+
known = self._atoms.setdefault(key, plain)
|
|
437
|
+
if known is not plain and known != plain:
|
|
438
|
+
a, b = _first_difference(known, plain)
|
|
439
|
+
# "have one key": as formulas the two atoms are written differently (a constant
|
|
440
|
+
# named like a variable is written in quotes), and it is the key, the text with
|
|
441
|
+
# every constant written by its name, that they share.
|
|
442
|
+
raise self._error(
|
|
443
|
+
f"{self._route}: two different atoms have one key and are both written "
|
|
444
|
+
f"{key!r}: one has the term {a!r} where the other has {b!r}. This route "
|
|
445
|
+
f"names an atom by its written form, so it would read them as one letter and "
|
|
446
|
+
f"answer about another problem. {_why_alike(a, b)}")
|
|
447
|
+
return key, plain, quoted
|
|
448
|
+
|
|
449
|
+
def letters(self, formulas: Iterable[Node]) -> List[str]:
|
|
450
|
+
"""The distinct keys of the atoms of ``formulas`` that are letters, in first-seen order.
|
|
451
|
+
|
|
452
|
+
The truth constants ``$true`` and ``$false`` are not letters and are left out.
|
|
453
|
+
"""
|
|
454
|
+
keys: List[str] = []
|
|
455
|
+
seen = set()
|
|
456
|
+
for formula in formulas:
|
|
457
|
+
for node in formula.walk():
|
|
458
|
+
if isinstance(node, Atom) and truth_value(node) is None:
|
|
459
|
+
key = self.key(node)
|
|
460
|
+
if key not in seen:
|
|
461
|
+
seen.add(key)
|
|
462
|
+
keys.append(key)
|
|
463
|
+
return keys
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def agent_key(agent: Node) -> str:
|
|
467
|
+
"""The name of the relation an operator of ``agent`` reads: its name, else its key text.
|
|
468
|
+
|
|
469
|
+
A modal operator of an agent (``K_a``, ``B_a``, ``Say_a``, ``Want_a``) reads the relation
|
|
470
|
+
named ``"K:" + agent_key(a)`` (and so on) of a model, the convention of
|
|
471
|
+
:mod:`~unicode_logic_kit.semantics.kripke`. A term without a name of its own (a numeral)
|
|
472
|
+
is keyed by its text with every constant written by its bare name, like an atom.
|
|
473
|
+
"""
|
|
474
|
+
return getattr(agent, "name", None) or key_text(agent)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _agents_of(node: Node) -> Tuple[Node, ...]:
|
|
478
|
+
"""The agent terms of one modal operator (none for any other node)."""
|
|
479
|
+
if isinstance(node, (Knows, Believes, Says, Wants)):
|
|
480
|
+
return (node.agent,)
|
|
481
|
+
if isinstance(node, (EverybodyKnows, DistributedKnowledge, CommonKnowledge)):
|
|
482
|
+
return tuple(node.group)
|
|
483
|
+
return ()
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def refuse_alike_agents(formulas: Iterable[Node], route: str) -> None:
|
|
487
|
+
"""Refuse two different agent terms that would name ONE relation, by name.
|
|
488
|
+
|
|
489
|
+
The operators of an agent are named by :func:`agent_key`, so two different terms with one
|
|
490
|
+
key (the numeral ``1`` and the constant ``'1'``; a free variable ``x`` and the constant
|
|
491
|
+
``x``) would be one agent on a route that files a relation under that name, and a formula
|
|
492
|
+
about two agents would be read as one about a single agent.
|
|
493
|
+
|
|
494
|
+
Raises:
|
|
495
|
+
NotImplementedError: two different agent terms of ``formulas`` have one
|
|
496
|
+
:func:`agent_key`.
|
|
497
|
+
"""
|
|
498
|
+
agents: Dict[str, Node] = {}
|
|
499
|
+
for formula in formulas:
|
|
500
|
+
for node in formula.walk():
|
|
501
|
+
for agent in _agents_of(node):
|
|
502
|
+
agent = _plain(agent) # ``a:S`` is the agent ``a``
|
|
503
|
+
key = agent_key(agent)
|
|
504
|
+
known = agents.setdefault(key, agent)
|
|
505
|
+
if known != agent:
|
|
506
|
+
raise NotImplementedError(
|
|
507
|
+
f"{route}: two different agents are both named {key!r}: {known!r} "
|
|
508
|
+
f"and {agent!r}. This route files the operators of an agent under "
|
|
509
|
+
f"the relation of that name, so it would read them as ONE agent and "
|
|
510
|
+
f"answer about another problem. {_why_alike(known, agent)}")
|