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.
@@ -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))