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,363 @@
1
+ """partition.py - Finite partition algebra over a declared carrier.
2
+
3
+ This module provides the lattice operations the interpretive kernel is built
4
+ from and knows nothing about interpretation, frames, probes, or occurrences.
5
+ It imports nothing else in this package.
6
+
7
+ A :class:`Partition` is an equivalence relation on a finite ordered carrier,
8
+ stored as a canonical block label for each carrier position.
9
+ Two partitions over the same carrier are equal exactly when they induce the
10
+ same blocks, regardless of how their blocks were originally labeled.
11
+
12
+ The order is refinement.
13
+ ``a.refines(b)`` holds when every pair related by ``a`` is also related by
14
+ ``b``, so finer partitions are lower.
15
+ The discrete partition is the bottom element and
16
+ the one-block partition is the top element.
17
+ Meet is intersection and join is the equivalence closure of the union.
18
+
19
+ This layer is written to be extractable.
20
+ It has no dependencies beyond the standard library and no vocabulary from the
21
+ layers above it.
22
+ Extraction is not justified until a second consumer needs this same public
23
+ surface.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from collections.abc import Hashable, Iterable, Iterator, Mapping, Sequence
29
+ from dataclasses import dataclass, field
30
+ from itertools import pairwise
31
+
32
+ type Item = Hashable
33
+ """One element of a carrier. Any hashable value may be used."""
34
+
35
+ type Pair = tuple[Item, Item]
36
+ """An unordered relationship between two carrier elements, written in order."""
37
+
38
+
39
+ class _UnionFind:
40
+ """Maintain disjoint carrier groups generated by supplied pairs.
41
+
42
+ Each element begins in its own group.
43
+ Joining two elements merges their groups,
44
+ and finding an element returns the representative used as that
45
+ group's label.
46
+
47
+ Path compression shortens previously traversed parent chains so later
48
+ lookups require less work.
49
+
50
+ This class is duplicated in spirit across the Structural Explainability
51
+ repositories on purpose.
52
+ Where two implementations exist to check each other,
53
+ sharing this code would remove the independence that makes the comparison
54
+ meaningful.
55
+ """
56
+
57
+ def __init__(self, items: Iterable[Item]) -> None:
58
+ """Create one singleton group for every supplied element."""
59
+ self.parent: dict[Item, Item] = {x: x for x in items}
60
+
61
+ def find(self, x: Item) -> Item:
62
+ """Return the representative of the group containing ``x``.
63
+
64
+ The first loop follows parent links to the current root.
65
+ The second loop compresses the traversed path
66
+ so those elements point directly to that root.
67
+ """
68
+ root = x
69
+ while self.parent[root] != root:
70
+ root = self.parent[root]
71
+ while self.parent[x] != root: # path compression
72
+ self.parent[x], x = root, self.parent[x]
73
+ return root
74
+
75
+ def union(self, a: Item, b: Item) -> None:
76
+ """Merge the groups containing ``a`` and ``b``.
77
+
78
+ Nothing changes when the two elements already belong to the same
79
+ group.
80
+ """
81
+ ra, rb = self.find(a), self.find(b)
82
+ if ra != rb:
83
+ self.parent[rb] = ra
84
+
85
+
86
+ def _canonical_labels(
87
+ carrier: Sequence[Item], raw: Mapping[Item, object]
88
+ ) -> tuple[int, ...]:
89
+ """Return block labels renumbered by first appearance in carrier order.
90
+
91
+ Any label values may be supplied.
92
+ They are replaced by integers assigned in the order their blocks are first
93
+ encountered, so two partitions with the same blocks receive the same
94
+ labels and compare equal.
95
+ """
96
+ seen: dict[object, int] = {}
97
+ out: list[int] = []
98
+ for item in carrier:
99
+ key = raw[item]
100
+ if key not in seen:
101
+ seen[key] = len(seen)
102
+ out.append(seen[key])
103
+ return tuple(out)
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class Partition:
108
+ """Represent one equivalence relation on a finite ordered carrier.
109
+
110
+ ``carrier`` is the finite domain, in a fixed order.
111
+ Carrier order determines canonical block numbering and the order in which
112
+ witnesses are searched, so results are deterministic.
113
+
114
+ ``labels`` gives the canonical block number of each carrier element, at
115
+ the same position as that element in ``carrier``.
116
+
117
+ Instances are constructed through the class methods rather than directly,
118
+ because those apply canonical renumbering.
119
+ """
120
+
121
+ carrier: tuple[Item, ...]
122
+ labels: tuple[int, ...]
123
+ _position: Mapping[Item, int] = field(
124
+ init=False, repr=False, compare=False, default_factory=dict
125
+ )
126
+
127
+ def __post_init__(self) -> None:
128
+ """Reject a malformed partition and build the position index.
129
+
130
+ Carrier elements must be distinct and every element must carry a
131
+ label.
132
+ """
133
+ if len(set(self.carrier)) != len(self.carrier):
134
+ msg = "carrier elements must be distinct"
135
+ raise ValueError(msg)
136
+ if len(self.labels) != len(self.carrier):
137
+ msg = "labels must supply one block number per carrier element"
138
+ raise ValueError(msg)
139
+ object.__setattr__(
140
+ self, "_position", {item: i for i, item in enumerate(self.carrier)}
141
+ )
142
+
143
+ # === Constructors ===
144
+
145
+ @classmethod
146
+ def from_labels(
147
+ cls, carrier: Sequence[Item], labels: Mapping[Item, object]
148
+ ) -> Partition:
149
+ """Return the partition grouping elements that share a label.
150
+
151
+ Label values are arbitrary and are renumbered canonically, so callers
152
+ may use whatever grouping key is natural for their domain.
153
+ """
154
+ seq = tuple(carrier)
155
+ return cls(seq, _canonical_labels(seq, labels))
156
+
157
+ @classmethod
158
+ def discrete(cls, carrier: Sequence[Item]) -> Partition:
159
+ """Return the finest partition, in which every element is alone.
160
+
161
+ This is the bottom element of the refinement order.
162
+ """
163
+ seq = tuple(carrier)
164
+ return cls(seq, tuple(range(len(seq))))
165
+
166
+ @classmethod
167
+ def indiscrete(cls, carrier: Sequence[Item]) -> Partition:
168
+ """Return the coarsest partition, in which all elements are together.
169
+
170
+ This is the top element of the refinement order.
171
+ """
172
+ seq = tuple(carrier)
173
+ return cls(seq, (0,) * len(seq))
174
+
175
+ @classmethod
176
+ def from_pairs(cls, carrier: Sequence[Item], pairs: Iterable[Pair]) -> Partition:
177
+ """Return the equivalence relation generated by the supplied pairs.
178
+
179
+ The supplied pairs need not be reflexive, symmetric, or transitive.
180
+ Their equivalence closure is computed by union-find, so only a
181
+ generating set has to be supplied.
182
+
183
+ This is the operation that makes the threshold construction cheap: a
184
+ spanning forest of a regime generates the same closure as the full
185
+ relation.
186
+ """
187
+ seq = tuple(carrier)
188
+ uf = _UnionFind(seq)
189
+ for x, y in pairs:
190
+ uf.union(x, y)
191
+ return cls.from_labels(seq, {item: uf.find(item) for item in seq})
192
+
193
+ @classmethod
194
+ def from_blocks(
195
+ cls, carrier: Sequence[Item], blocks: Iterable[Iterable[Item]]
196
+ ) -> Partition:
197
+ """Return the partition with the supplied blocks.
198
+
199
+ Elements of the carrier not named in any supplied block are placed in
200
+ singleton blocks of their own.
201
+ """
202
+ seq = tuple(carrier)
203
+ pairs: list[Pair] = []
204
+ for block in blocks:
205
+ members = list(block)
206
+ pairs.extend(pairwise(members))
207
+ return cls.from_pairs(seq, pairs)
208
+
209
+ # === Queries ===
210
+
211
+ def blocks(self) -> tuple[tuple[Item, ...], ...]:
212
+ """Return the blocks, each in carrier order, ordered by first member."""
213
+ groups: dict[int, list[Item]] = {}
214
+ for item, label in zip(self.carrier, self.labels, strict=True):
215
+ groups.setdefault(label, []).append(item)
216
+ return tuple(tuple(groups[k]) for k in sorted(groups))
217
+
218
+ def label_of(self, item: Item) -> int:
219
+ """Return the canonical block number of one carrier element."""
220
+ return self.labels[self._position[item]]
221
+
222
+ def same(self, x: Item, y: Item) -> bool:
223
+ """Return whether two carrier elements lie in the same block."""
224
+ return self.label_of(x) == self.label_of(y)
225
+
226
+ def refines(self, other: Partition) -> bool:
227
+ """Return whether this partition refines ``other``.
228
+
229
+ Refinement holds when every pair this partition relates is also
230
+ related by ``other``.
231
+ A finer partition may make additional distinctions,
232
+ but it may not merge elements the coarser partition separates.
233
+
234
+ The check records the first ``other`` label seen for each of this
235
+ partition's labels and fails on a later conflict, so no pair of
236
+ carrier elements is enumerated.
237
+ """
238
+ self._require_same_carrier(other)
239
+ seen: dict[int, int] = {}
240
+ for item in self.carrier:
241
+ fine, coarse = self.label_of(item), other.label_of(item)
242
+ if fine in seen:
243
+ if seen[fine] != coarse:
244
+ return False
245
+ else:
246
+ seen[fine] = coarse
247
+ return True
248
+
249
+ def refinement_witness(self, other: Partition) -> Pair | None:
250
+ """Return a pair showing why this partition does not refine ``other``.
251
+
252
+ The returned pair is related by this partition and separated by
253
+ ``other``.
254
+ Carrier order is searched in order and the first such pair is
255
+ returned, so the witness is deterministic.
256
+
257
+ None is returned when this partition does refine ``other``.
258
+ """
259
+ self._require_same_carrier(other)
260
+ anchor: dict[int, Item] = {}
261
+ for item in self.carrier:
262
+ fine = self.label_of(item)
263
+ if fine not in anchor:
264
+ anchor[fine] = item
265
+ elif not other.same(anchor[fine], item):
266
+ return (anchor[fine], item)
267
+ return None
268
+
269
+ # === Lattice operations ===
270
+
271
+ def meet(self, other: Partition) -> Partition:
272
+ """Return the greatest lower bound: elements related by both.
273
+
274
+ Meet is intersection.
275
+ The result relates two elements exactly when this partition and
276
+ ``other`` both relate them.
277
+ """
278
+ self._require_same_carrier(other)
279
+ return Partition.from_labels(
280
+ self.carrier,
281
+ {
282
+ item: (self.label_of(item), other.label_of(item))
283
+ for item in self.carrier
284
+ },
285
+ )
286
+
287
+ def join(self, other: Partition) -> Partition:
288
+ """Return the least upper bound: the closure of both relations.
289
+
290
+ Join is the equivalence closure of the union.
291
+ Each partition contributes a spanning path through every one of its
292
+ blocks, which generates the same closure as the full relations.
293
+ """
294
+ self._require_same_carrier(other)
295
+ pairs: list[Pair] = []
296
+ for part in (self, other):
297
+ for block in part.blocks():
298
+ pairs.extend(pairwise(block))
299
+ return Partition.from_pairs(self.carrier, pairs)
300
+
301
+ def _require_same_carrier(self, other: Partition) -> None:
302
+ """Reject a comparison between partitions over different carriers.
303
+
304
+ Refinement is only defined between partitions of one domain.
305
+ Comparing across carriers is the mistyping this guard exists to
306
+ surface rather than silently permit.
307
+ """
308
+ if self.carrier != other.carrier:
309
+ msg = "partitions must be over the same carrier, in the same order"
310
+ raise ValueError(msg)
311
+
312
+
313
+ def all_partitions(carrier: Sequence[Item]) -> Iterator[Partition]:
314
+ """Yield every partition of a finite carrier, once each.
315
+
316
+ Partitions are generated as restricted growth strings, which are already
317
+ the canonical labeling, so each partition is produced exactly once.
318
+
319
+ The count is the Bell number of the carrier size and grows quickly.
320
+ This is intended for exhaustive law checking over the small carriers the
321
+ counterexamples and property tests use, not for enumeration at scale.
322
+ """
323
+ seq = tuple(carrier)
324
+ size = len(seq)
325
+ if size == 0:
326
+ yield Partition(seq, ())
327
+ return
328
+
329
+ def grow(prefix: list[int], used: int) -> Iterator[tuple[int, ...]]:
330
+ if len(prefix) == size:
331
+ yield tuple(prefix)
332
+ return
333
+ for label in range(used + 1):
334
+ prefix.append(label)
335
+ yield from grow(prefix, max(used, label + 1))
336
+ prefix.pop()
337
+
338
+ for labels in grow([], 0):
339
+ yield Partition(seq, labels)
340
+
341
+
342
+ def meet_all(carrier: Sequence[Item], parts: Iterable[Partition]) -> Partition:
343
+ """Return the meet of every supplied partition over a carrier.
344
+
345
+ An empty collection yields the top element, which is the identity for
346
+ meet.
347
+ """
348
+ result = Partition.indiscrete(carrier)
349
+ for part in parts:
350
+ result = result.meet(part)
351
+ return result
352
+
353
+
354
+ def join_all(carrier: Sequence[Item], parts: Iterable[Partition]) -> Partition:
355
+ """Return the join of every supplied partition over a carrier.
356
+
357
+ An empty collection yields the bottom element, which is the identity for
358
+ join.
359
+ """
360
+ result = Partition.discrete(carrier)
361
+ for part in parts:
362
+ result = result.join(part)
363
+ return result
@@ -0,0 +1,124 @@
1
+ """probes.py - Probe sensitivity of the kernel and the threshold.
2
+
3
+ Every result in this package is relative to the declared probe family Q. This
4
+ module reports how much each result depends on which probes are in Q.
5
+
6
+ For the kernel K_Q(rho):
7
+
8
+ - :func:`separating_probes` lists the probes that tell two frames apart.
9
+ - :func:`sole_separated_pairs` lists, for each probe, the frame pairs that no
10
+ other selected probe separates.
11
+ - :func:`load_bearing_probes` lists the probes whose removal changes the
12
+ kernel.
13
+ - :func:`minimal_probe_sets` lists every smallest-by-inclusion subset of Q that
14
+ gives the same kernel.
15
+
16
+ For the threshold L_Q(phi):
17
+
18
+ - :func:`probe_contributions` gives the threshold each probe forces alone. By
19
+ probe-union duality (Lean: ``liftRel_probeUnion``), L_Q(phi) is the join of
20
+ these.
21
+
22
+ Nothing here decides whether the probe family is complete. It reports how the
23
+ answer moves when probes are added or removed.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from collections.abc import Sequence
29
+ from itertools import combinations
30
+
31
+ from .artifact import Artifact
32
+ from .generated import generated
33
+ from .kernel import kernel
34
+ from .partition import Item, Partition
35
+
36
+
37
+ def separating_probes(
38
+ artifact: Artifact,
39
+ selected: Sequence[Item],
40
+ rho: Partition,
41
+ x: Item,
42
+ y: Item,
43
+ ) -> tuple[Item, ...]:
44
+ """Return the selected probes whose outputs for x and y rho does not relate.
45
+
46
+ The frames x and y are related by K_Q(rho) exactly when this is empty.
47
+ """
48
+ artifact.require_selected(selected)
49
+ if rho.carrier != artifact.outputs:
50
+ msg = "rho must be a partition of the artifact's outputs, in order"
51
+ raise ValueError(msg)
52
+ return tuple(
53
+ probe
54
+ for probe in selected
55
+ if not rho.same(artifact.interpret(probe, x), artifact.interpret(probe, y))
56
+ )
57
+
58
+
59
+ def sole_separated_pairs(
60
+ artifact: Artifact, selected: Sequence[Item], rho: Partition
61
+ ) -> dict[Item, list[tuple[Item, Item]]]:
62
+ """Return, for each selected probe, the frame pairs only that probe separates.
63
+
64
+ A pair listed under probe p is related by K_Q(rho) minus p and separated by
65
+ K_Q(rho): removing p from the family would merge the two frames. Pairs are
66
+ listed once, in frame order. Every selected probe appears as a key, with an
67
+ empty list when it is the sole separator of nothing.
68
+ """
69
+ result: dict[Item, list[tuple[Item, Item]]] = {probe: [] for probe in selected}
70
+ frames = artifact.frames
71
+ for i, x in enumerate(frames):
72
+ for y in frames[i + 1 :]:
73
+ separators = separating_probes(artifact, selected, rho, x, y)
74
+ if len(separators) == 1:
75
+ result[separators[0]].append((x, y))
76
+ return result
77
+
78
+
79
+ def load_bearing_probes(
80
+ artifact: Artifact, selected: Sequence[Item], rho: Partition
81
+ ) -> tuple[Item, ...]:
82
+ """Return the selected probes whose removal changes K_Q(rho).
83
+
84
+ Because the kernel only gets coarser as probes are removed, a load-bearing
85
+ probe belongs to every minimal probe set.
86
+ """
87
+ full = kernel(artifact, selected, rho)
88
+ return tuple(
89
+ probe
90
+ for probe in selected
91
+ if kernel(artifact, [p for p in selected if p != probe], rho) != full
92
+ )
93
+
94
+
95
+ def minimal_probe_sets(
96
+ artifact: Artifact, selected: Sequence[Item], rho: Partition
97
+ ) -> list[tuple[Item, ...]]:
98
+ """Return every subset of Q that gives the same kernel and has no smaller one.
99
+
100
+ The search is exhaustive over subsets, smallest first, so it is intended
101
+ for small probe families. :func:`load_bearing_probes` and
102
+ :func:`sole_separated_pairs` scale polynomially and give the same
103
+ information about single probes.
104
+ """
105
+ target = kernel(artifact, selected, rho)
106
+ found: list[tuple[Item, ...]] = []
107
+ for size in range(len(selected) + 1):
108
+ for subset in combinations(selected, size):
109
+ if any(set(smaller) <= set(subset) for smaller in found):
110
+ continue
111
+ if kernel(artifact, subset, rho) == target:
112
+ found.append(subset)
113
+ return found
114
+
115
+
116
+ def probe_contributions(
117
+ artifact: Artifact, selected: Sequence[Item], phi: Partition
118
+ ) -> dict[Item, Partition]:
119
+ """Return, for each selected probe, the threshold that probe forces alone.
120
+
121
+ The threshold of the whole family is the join of these partitions.
122
+ """
123
+ artifact.require_selected(selected)
124
+ return {probe: generated(artifact, (probe,), phi) for probe in selected}
@@ -0,0 +1,36 @@
1
+ """sampling.py - Random small artifacts for property checks.
2
+
3
+ Deterministic for a given seed. Sizes are kept small so that exhaustive
4
+ enumeration of partitions of the outputs and the frames stays cheap.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Iterator
10
+ from random import Random
11
+
12
+ from .artifact import Artifact
13
+ from .partition import Item
14
+
15
+
16
+ def random_artifact(
17
+ rng: Random, max_frames: int = 4, max_outputs: int = 4, max_probes: int = 3
18
+ ) -> Artifact:
19
+ """Return a random total artifact of small size."""
20
+ n_frames = rng.randint(1, max_frames)
21
+ n_outputs = rng.randint(1, max_outputs)
22
+ n_probes = rng.randint(0, max_probes)
23
+ frames = tuple(range(n_frames))
24
+ outputs = tuple(range(n_outputs))
25
+ probes = tuple(f"p{i}" for i in range(n_probes))
26
+ table: dict[tuple[Item, Item], Item] = {
27
+ (p, f): rng.choice(outputs) for p in probes for f in frames
28
+ }
29
+ return Artifact(frames, outputs, probes, table, "random")
30
+
31
+
32
+ def random_artifacts(count: int, seed: int) -> Iterator[Artifact]:
33
+ """Yield ``count`` random artifacts for the given seed."""
34
+ rng = Random(seed)
35
+ for _ in range(count):
36
+ yield random_artifact(rng)
@@ -0,0 +1,69 @@
1
+ """witness.py - Finite incompatibility witnesses.
2
+
3
+ A witness against coherence of phi with rho (Lean:
4
+ ``IncompatibilityWitness``) consists of two frames related by phi, a selected
5
+ probe, and the two outputs, which rho does not relate.
6
+
7
+ A witness shows that the supplied phi, rho, probes, and interpretation are
8
+ incompatible. It does not show which of them is the faulty choice.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections.abc import Sequence
14
+ from dataclasses import dataclass
15
+
16
+ from .artifact import Artifact
17
+ from .partition import Item, Partition
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Witness:
22
+ """Contain one finite incompatibility witness."""
23
+
24
+ frame_1: Item
25
+ frame_2: Item
26
+ probe: Item
27
+ output_1: Item
28
+ output_2: Item
29
+
30
+
31
+ def find_witness(
32
+ artifact: Artifact, selected: Sequence[Item], phi: Partition, rho: Partition
33
+ ) -> Witness | None:
34
+ """Return a witness against coherence, or None when phi is coherent.
35
+
36
+ Within each block of phi the first frame is compared with every later
37
+ frame. This finds a witness whenever one exists: if the first frame agrees
38
+ with each later frame under every selected probe, transitivity of rho gives
39
+ agreement for every pair in the block.
40
+ """
41
+ artifact.require_selected(selected)
42
+ for block in phi.blocks():
43
+ anchor = block[0]
44
+ for frame in block[1:]:
45
+ for probe in selected:
46
+ out_anchor = artifact.interpret(probe, anchor)
47
+ out_frame = artifact.interpret(probe, frame)
48
+ if not rho.same(out_anchor, out_frame):
49
+ return Witness(anchor, frame, probe, out_anchor, out_frame)
50
+ return None
51
+
52
+
53
+ def witness_is_valid(
54
+ artifact: Artifact,
55
+ selected: Sequence[Item],
56
+ phi: Partition,
57
+ rho: Partition,
58
+ witness: Witness,
59
+ ) -> bool:
60
+ """Return whether a witness satisfies the definition of a witness."""
61
+ if witness.probe not in set(selected):
62
+ return False
63
+ if not phi.same(witness.frame_1, witness.frame_2):
64
+ return False
65
+ if artifact.interpret(witness.probe, witness.frame_1) != witness.output_1:
66
+ return False
67
+ if artifact.interpret(witness.probe, witness.frame_2) != witness.output_2:
68
+ return False
69
+ return not rho.same(witness.output_1, witness.output_2)