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