se-interpretive-kernel 0.1.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.
- se_interpretive_kernel/__init__.py +7 -0
- se_interpretive_kernel/_version.py +24 -0
- se_interpretive_kernel/artifact.py +76 -0
- se_interpretive_kernel/cases.py +120 -0
- se_interpretive_kernel/check.py +293 -0
- se_interpretive_kernel/cli.py +5 -0
- se_interpretive_kernel/demand.py +66 -0
- se_interpretive_kernel/differences.py +69 -0
- se_interpretive_kernel/explain.py +224 -0
- se_interpretive_kernel/generated.py +100 -0
- se_interpretive_kernel/kernel.py +52 -0
- se_interpretive_kernel/lifting.py +374 -0
- se_interpretive_kernel/literal.py +87 -0
- se_interpretive_kernel/loader.py +238 -0
- se_interpretive_kernel/partition.py +363 -0
- se_interpretive_kernel/probes.py +124 -0
- se_interpretive_kernel/sampling.py +36 -0
- se_interpretive_kernel/witness.py +69 -0
- se_interpretive_kernel-0.1.0.dist-info/METADATA +261 -0
- se_interpretive_kernel-0.1.0.dist-info/RECORD +23 -0
- se_interpretive_kernel-0.1.0.dist-info/WHEEL +4 -0
- se_interpretive_kernel-0.1.0.dist-info/entry_points.txt +2 -0
- se_interpretive_kernel-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""explain.py - Derivations for the identifications a threshold forces.
|
|
2
|
+
|
|
3
|
+
The generated occurrence regime L_Q(phi) is an equivalence closure, so two
|
|
4
|
+
outputs are related only because a chain of generating pairs connects them.
|
|
5
|
+
This module returns that chain. Each step says which probe read which two
|
|
6
|
+
frames, that the frame regime relates those frames, and which two outputs the
|
|
7
|
+
probe produced.
|
|
8
|
+
|
|
9
|
+
An explanation is a finite derivation of one forced identification. It shows why
|
|
10
|
+
the declared frame regime obliges the outputs to be called the same under the
|
|
11
|
+
declared probes. It does not show that the identification is wrong or right.
|
|
12
|
+
|
|
13
|
+
The chain is found in the graph of spanning-path generating pairs (Lean:
|
|
14
|
+
``liftRel_eq_of_generates``), so every frame pair named in a step lies in one
|
|
15
|
+
block of phi. The chain is a shortest one in that graph, not necessarily the
|
|
16
|
+
shortest over all frame pairs.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from collections import deque
|
|
22
|
+
from collections.abc import Sequence
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
|
|
25
|
+
from .artifact import Artifact
|
|
26
|
+
from .generated import generated
|
|
27
|
+
from .partition import Item, Partition
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Step:
|
|
32
|
+
"""Contain one link of a derivation.
|
|
33
|
+
|
|
34
|
+
``probe`` reads ``frame_1`` as ``output_from`` and ``frame_2`` as
|
|
35
|
+
``output_to``, and the frame regime relates ``frame_1`` and ``frame_2``.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
probe: Item
|
|
39
|
+
frame_1: Item
|
|
40
|
+
frame_2: Item
|
|
41
|
+
output_from: Item
|
|
42
|
+
output_to: Item
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class Explanation:
|
|
47
|
+
"""Contain the derivation of one forced identification."""
|
|
48
|
+
|
|
49
|
+
output_1: Item
|
|
50
|
+
output_2: Item
|
|
51
|
+
steps: tuple[Step, ...]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _check_outputs(artifact: Artifact, *outputs: Item) -> None:
|
|
55
|
+
"""Reject an output that the artifact does not have."""
|
|
56
|
+
known = set(artifact.outputs)
|
|
57
|
+
for output in outputs:
|
|
58
|
+
if output not in known:
|
|
59
|
+
msg = f"{output!r} is not an output of this artifact"
|
|
60
|
+
raise ValueError(msg)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _build_edges(
|
|
64
|
+
artifact: Artifact, selected: Sequence[Item], phi: Partition
|
|
65
|
+
) -> dict[Item, list[Step]]:
|
|
66
|
+
"""Return, for each output, the steps that leave it.
|
|
67
|
+
|
|
68
|
+
Built from spanning-path generating pairs. Each pair gives a step in both
|
|
69
|
+
directions.
|
|
70
|
+
"""
|
|
71
|
+
edges: dict[Item, list[Step]] = {}
|
|
72
|
+
for block in phi.blocks():
|
|
73
|
+
for index in range(len(block) - 1):
|
|
74
|
+
first = block[index]
|
|
75
|
+
second = block[index + 1]
|
|
76
|
+
for probe in selected:
|
|
77
|
+
out_first = artifact.interpret(probe, first)
|
|
78
|
+
out_second = artifact.interpret(probe, second)
|
|
79
|
+
if out_first == out_second:
|
|
80
|
+
continue
|
|
81
|
+
edges.setdefault(out_first, []).append(
|
|
82
|
+
Step(probe, first, second, out_first, out_second)
|
|
83
|
+
)
|
|
84
|
+
edges.setdefault(out_second, []).append(
|
|
85
|
+
Step(probe, second, first, out_second, out_first)
|
|
86
|
+
)
|
|
87
|
+
return edges
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _search_from(
|
|
91
|
+
edges: dict[Item, list[Step]], start: Item, goal: Item | None = None
|
|
92
|
+
) -> dict[Item, Step]:
|
|
93
|
+
"""Return the last step on a shortest path from start to each reached output.
|
|
94
|
+
|
|
95
|
+
Stops early once ``goal`` is reached. With no goal it reaches everything
|
|
96
|
+
connected to ``start``.
|
|
97
|
+
"""
|
|
98
|
+
arrived_by: dict[Item, Step] = {}
|
|
99
|
+
queue: deque[Item] = deque([start])
|
|
100
|
+
seen: set[Item] = {start}
|
|
101
|
+
while queue:
|
|
102
|
+
current = queue.popleft()
|
|
103
|
+
for step in edges.get(current, []):
|
|
104
|
+
if step.output_to in seen:
|
|
105
|
+
continue
|
|
106
|
+
seen.add(step.output_to)
|
|
107
|
+
arrived_by[step.output_to] = step
|
|
108
|
+
if step.output_to == goal:
|
|
109
|
+
return arrived_by
|
|
110
|
+
queue.append(step.output_to)
|
|
111
|
+
return arrived_by
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _path_to(
|
|
115
|
+
arrived_by: dict[Item, Step], start: Item, goal: Item
|
|
116
|
+
) -> list[Step] | None:
|
|
117
|
+
"""Return the steps from start to goal, or None if goal was not reached."""
|
|
118
|
+
if goal not in arrived_by:
|
|
119
|
+
return None
|
|
120
|
+
path: list[Step] = []
|
|
121
|
+
node = goal
|
|
122
|
+
while node != start:
|
|
123
|
+
link = arrived_by[node]
|
|
124
|
+
path.append(link)
|
|
125
|
+
node = link.output_from
|
|
126
|
+
path.reverse()
|
|
127
|
+
return path
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def explain(
|
|
131
|
+
artifact: Artifact,
|
|
132
|
+
selected: Sequence[Item],
|
|
133
|
+
phi: Partition,
|
|
134
|
+
a: Item,
|
|
135
|
+
b: Item,
|
|
136
|
+
) -> list[Step] | None:
|
|
137
|
+
"""Return a derivation that L_Q(phi) relates a and b, or None if it does not.
|
|
138
|
+
|
|
139
|
+
An empty list means a and b are the same output.
|
|
140
|
+
"""
|
|
141
|
+
artifact.require_selected(selected)
|
|
142
|
+
if phi.carrier != artifact.frames:
|
|
143
|
+
msg = "phi must be a partition of the artifact's frames, in order"
|
|
144
|
+
raise ValueError(msg)
|
|
145
|
+
_check_outputs(artifact, a, b)
|
|
146
|
+
if a == b:
|
|
147
|
+
return []
|
|
148
|
+
|
|
149
|
+
edges = _build_edges(artifact, selected, phi)
|
|
150
|
+
return _path_to(_search_from(edges, a, b), a, b)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def steps_are_valid(
|
|
154
|
+
artifact: Artifact,
|
|
155
|
+
selected: Sequence[Item],
|
|
156
|
+
phi: Partition,
|
|
157
|
+
a: Item,
|
|
158
|
+
b: Item,
|
|
159
|
+
steps: Sequence[Step],
|
|
160
|
+
) -> bool:
|
|
161
|
+
"""Return whether steps form a derivation from a to b.
|
|
162
|
+
|
|
163
|
+
Checks the definition directly: the chain starts at a, ends at b, each link
|
|
164
|
+
starts where the previous one ended, every probe is selected, the frame
|
|
165
|
+
regime relates the two frames of each link, and each output is what the
|
|
166
|
+
probe produces.
|
|
167
|
+
"""
|
|
168
|
+
if not steps:
|
|
169
|
+
return a == b
|
|
170
|
+
if steps[0].output_from != a or steps[-1].output_to != b:
|
|
171
|
+
return False
|
|
172
|
+
chosen = set(selected)
|
|
173
|
+
previous = a
|
|
174
|
+
for step in steps:
|
|
175
|
+
if step.output_from != previous:
|
|
176
|
+
return False
|
|
177
|
+
if step.probe not in chosen:
|
|
178
|
+
return False
|
|
179
|
+
if not phi.same(step.frame_1, step.frame_2):
|
|
180
|
+
return False
|
|
181
|
+
if artifact.interpret(step.probe, step.frame_1) != step.output_from:
|
|
182
|
+
return False
|
|
183
|
+
if artifact.interpret(step.probe, step.frame_2) != step.output_to:
|
|
184
|
+
return False
|
|
185
|
+
previous = step.output_to
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def explain_threshold(
|
|
190
|
+
artifact: Artifact, selected: Sequence[Item], phi: Partition
|
|
191
|
+
) -> list[Explanation]:
|
|
192
|
+
"""Return a derivation for every identification the threshold forces.
|
|
193
|
+
|
|
194
|
+
For each block of L_Q(phi) with more than one output, the first output of
|
|
195
|
+
the block is explained against each other output in it. A block of size n
|
|
196
|
+
gives n - 1 explanations, which together connect the whole block.
|
|
197
|
+
"""
|
|
198
|
+
threshold = generated(artifact, selected, phi)
|
|
199
|
+
edges = _build_edges(artifact, selected, phi)
|
|
200
|
+
explanations: list[Explanation] = []
|
|
201
|
+
for block in threshold.blocks():
|
|
202
|
+
if len(block) < 2:
|
|
203
|
+
continue
|
|
204
|
+
anchor = block[0]
|
|
205
|
+
arrived_by = _search_from(edges, anchor)
|
|
206
|
+
for other in block[1:]:
|
|
207
|
+
steps = _path_to(arrived_by, anchor, other)
|
|
208
|
+
if steps is None:
|
|
209
|
+
msg = "threshold relates outputs that no derivation connects"
|
|
210
|
+
raise RuntimeError(msg)
|
|
211
|
+
explanations.append(Explanation(anchor, other, tuple(steps)))
|
|
212
|
+
return explanations
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def format_steps(steps: Sequence[Step]) -> str:
|
|
216
|
+
"""Return a derivation as plain text, one line per link."""
|
|
217
|
+
lines: list[str] = []
|
|
218
|
+
for number, step in enumerate(steps, start=1):
|
|
219
|
+
lines.append(
|
|
220
|
+
f"{number}. probe {step.probe!r} reads frame {step.frame_1!r} as "
|
|
221
|
+
f"{step.output_from!r} and frame {step.frame_2!r} as "
|
|
222
|
+
f"{step.output_to!r}; the frame regime relates those two frames"
|
|
223
|
+
)
|
|
224
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""generated.py - The generated occurrence regime L_Q(phi).
|
|
2
|
+
|
|
3
|
+
Definition (Lean: ``liftRel``). For a frame regime phi and a selected probe
|
|
4
|
+
family Q, the generating pairs are
|
|
5
|
+
|
|
6
|
+
(I(p,F), I(p,F')) for selected p and F phi F'.
|
|
7
|
+
|
|
8
|
+
L_Q(phi) is the equivalence closure of the generating pairs, the least
|
|
9
|
+
occurrence regime that makes phi coherent.
|
|
10
|
+
|
|
11
|
+
Terminology. In this module and in the Lean, "generated occurrence regime"
|
|
12
|
+
names L_Q(phi). The paper's "comparison lifting" is a different construction;
|
|
13
|
+
see ``lifting.py``.
|
|
14
|
+
|
|
15
|
+
Generating-set reduction (Lean: ``liftRel_eq_of_generates``). It is enough to
|
|
16
|
+
use a spanning path through each block of phi instead of all pairs in the
|
|
17
|
+
block. :func:`generated` does that; :func:`generated_all_pairs` uses every
|
|
18
|
+
pair and exists so the two can be compared.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from collections.abc import Sequence
|
|
24
|
+
|
|
25
|
+
from .artifact import Artifact
|
|
26
|
+
from .partition import Item, Pair, Partition
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _check_inputs(artifact: Artifact, selected: Sequence[Item], phi: Partition) -> None:
|
|
30
|
+
"""Reject a selection or a frame regime that does not fit the artifact."""
|
|
31
|
+
artifact.require_selected(selected)
|
|
32
|
+
if phi.carrier != artifact.frames:
|
|
33
|
+
msg = "phi must be a partition of the artifact's frames, in order"
|
|
34
|
+
raise ValueError(msg)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def generating_pairs_reduced(
|
|
38
|
+
artifact: Artifact, selected: Sequence[Item], phi: Partition
|
|
39
|
+
) -> list[Pair]:
|
|
40
|
+
"""Return generating pairs from a spanning path through each phi block."""
|
|
41
|
+
_check_inputs(artifact, selected, phi)
|
|
42
|
+
pairs: list[Pair] = []
|
|
43
|
+
for block in phi.blocks():
|
|
44
|
+
for index in range(len(block) - 1):
|
|
45
|
+
first = block[index]
|
|
46
|
+
second = block[index + 1]
|
|
47
|
+
for probe in selected:
|
|
48
|
+
pairs.append(
|
|
49
|
+
(
|
|
50
|
+
artifact.interpret(probe, first),
|
|
51
|
+
artifact.interpret(probe, second),
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
return pairs
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def generating_pairs_all(
|
|
58
|
+
artifact: Artifact, selected: Sequence[Item], phi: Partition
|
|
59
|
+
) -> list[Pair]:
|
|
60
|
+
"""Return generating pairs from every ordered pair inside each phi block."""
|
|
61
|
+
_check_inputs(artifact, selected, phi)
|
|
62
|
+
pairs: list[Pair] = []
|
|
63
|
+
for block in phi.blocks():
|
|
64
|
+
for first in block:
|
|
65
|
+
for second in block:
|
|
66
|
+
for probe in selected:
|
|
67
|
+
pairs.append(
|
|
68
|
+
(
|
|
69
|
+
artifact.interpret(probe, first),
|
|
70
|
+
artifact.interpret(probe, second),
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
return pairs
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def generated(
|
|
77
|
+
artifact: Artifact, selected: Sequence[Item], phi: Partition
|
|
78
|
+
) -> Partition:
|
|
79
|
+
"""Return L_Q(phi) using the spanning-path generating set."""
|
|
80
|
+
pairs = generating_pairs_reduced(artifact, selected, phi)
|
|
81
|
+
return Partition.from_pairs(artifact.outputs, pairs)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def generated_all_pairs(
|
|
85
|
+
artifact: Artifact, selected: Sequence[Item], phi: Partition
|
|
86
|
+
) -> Partition:
|
|
87
|
+
"""Return L_Q(phi) using every pair inside each block of phi."""
|
|
88
|
+
pairs = generating_pairs_all(artifact, selected, phi)
|
|
89
|
+
return Partition.from_pairs(artifact.outputs, pairs)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def threshold(
|
|
93
|
+
artifact: Artifact, selected: Sequence[Item], phi: Partition
|
|
94
|
+
) -> Partition:
|
|
95
|
+
"""Return the interpretive threshold, which is L_Q(phi).
|
|
96
|
+
|
|
97
|
+
An occurrence regime rho is admissible for phi exactly when this partition
|
|
98
|
+
refines rho (Lean: ``admissible_iff_threshold_finer``).
|
|
99
|
+
"""
|
|
100
|
+
return generated(artifact, selected, phi)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""kernel.py - The interpretation-induced kernel K_Q(rho) and coherence.
|
|
2
|
+
|
|
3
|
+
Definition (Lean: ``kernelRel``). For a selected probe family Q and an
|
|
4
|
+
occurrence regime rho on the outputs,
|
|
5
|
+
|
|
6
|
+
F K_Q(rho) F' iff for every selected probe p, I(p,F) rho I(p,F').
|
|
7
|
+
|
|
8
|
+
Construction. This module builds the kernel from probe signatures: each frame
|
|
9
|
+
is labeled by the tuple of rho-block numbers its selected probes produce, and
|
|
10
|
+
frames with equal labels share a block. This is the kernel of the signature
|
|
11
|
+
map, which is the paper's definition.
|
|
12
|
+
|
|
13
|
+
With no selected probes every frame has the empty signature, so the kernel is
|
|
14
|
+
the indiscrete partition (Lean: ``kernelRel_noProbes``).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
|
|
21
|
+
from .artifact import Artifact
|
|
22
|
+
from .partition import Item, Partition
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def kernel(artifact: Artifact, selected: Sequence[Item], rho: Partition) -> Partition:
|
|
26
|
+
"""Return K_Q(rho), a partition of the frames.
|
|
27
|
+
|
|
28
|
+
``selected`` is the selected probe family Q, a subset of the artifact's
|
|
29
|
+
probes. ``rho`` is a partition of the artifact's outputs.
|
|
30
|
+
"""
|
|
31
|
+
artifact.require_selected(selected)
|
|
32
|
+
if rho.carrier != artifact.outputs:
|
|
33
|
+
msg = "rho must be a partition of the artifact's outputs, in order"
|
|
34
|
+
raise ValueError(msg)
|
|
35
|
+
signature: dict[Item, object] = {}
|
|
36
|
+
for frame in artifact.frames:
|
|
37
|
+
signature[frame] = tuple(
|
|
38
|
+
rho.label_of(artifact.interpret(probe, frame)) for probe in selected
|
|
39
|
+
)
|
|
40
|
+
return Partition.from_labels(artifact.frames, signature)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def coherent(
|
|
44
|
+
artifact: Artifact, selected: Sequence[Item], phi: Partition, rho: Partition
|
|
45
|
+
) -> bool:
|
|
46
|
+
"""Return whether phi is coherent with rho: phi refines K_Q(rho).
|
|
47
|
+
|
|
48
|
+
Coherence is mathematical compatibility only. It holds trivially when rho
|
|
49
|
+
is indiscrete or when no probes are selected, so it is informative only
|
|
50
|
+
relative to a justified rho and Q.
|
|
51
|
+
"""
|
|
52
|
+
return phi.refines(kernel(artifact, selected, rho))
|