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,7 @@
1
+ """SE Interpretive Kernel (SE-220).
2
+
3
+ A companion to the paper, providing executable finite constructions and checks
4
+ for the interpretation-induced kernel, the generated occurrence regime, their
5
+ adjunction, interpretive thresholds, pairwise demand, and incompatibility
6
+ witnesses.
7
+ """
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,76 @@
1
+ """artifact.py - Finite comparison artifact for the Interpretive Kernel.
2
+
3
+ An :class:`Artifact` holds the data the theory starts from:
4
+
5
+ - a finite carrier of frames (the paper's E),
6
+ - a finite carrier of outputs (the paper's A),
7
+ - a finite set of probes (the paper's P), and
8
+ - a total interpretation table I : P x E -> A.
9
+
10
+ It holds no equivalence relations. Frame regimes and output regimes are
11
+ :class:`~se_interpretive_kernel.partition.Partition` values supplied
12
+ separately, over ``frames`` and ``outputs`` in the order stored here.
13
+
14
+ Nothing here decides which probes, frames, or outputs a domain ought to
15
+ declare, and nothing here shows that an interpretation is correct.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from collections.abc import Mapping, Sequence
21
+ from dataclasses import dataclass
22
+
23
+ from se_interpretive_kernel.partition import Item
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class Artifact:
28
+ """Contain the finite data of one comparison.
29
+
30
+ ``table`` maps ``(probe, frame)`` to an output. It must be defined for
31
+ every probe and frame, and every value must be an output. Totality is
32
+ required by the theory: a missing or inapplicable result must be
33
+ represented by a declared output value.
34
+
35
+ ``label`` is a human-readable name for diagnostics.
36
+ """
37
+
38
+ frames: tuple[Item, ...]
39
+ outputs: tuple[Item, ...]
40
+ probes: tuple[Item, ...]
41
+ table: Mapping[tuple[Item, Item], Item]
42
+ label: str = ""
43
+
44
+ def __post_init__(self) -> None:
45
+ """Reject malformed data: duplicates, missing entries, unknown outputs."""
46
+ if len(set(self.frames)) != len(self.frames):
47
+ msg = "frames must be distinct"
48
+ raise ValueError(msg)
49
+ if len(set(self.outputs)) != len(self.outputs):
50
+ msg = "outputs must be distinct"
51
+ raise ValueError(msg)
52
+ if len(set(self.probes)) != len(self.probes):
53
+ msg = "probes must be distinct"
54
+ raise ValueError(msg)
55
+ output_set = set(self.outputs)
56
+ for probe in self.probes:
57
+ for frame in self.frames:
58
+ key = (probe, frame)
59
+ if key not in self.table:
60
+ msg = f"interpretation is not total: missing {key!r}"
61
+ raise ValueError(msg)
62
+ if self.table[key] not in output_set:
63
+ msg = f"interpretation value {self.table[key]!r} is not an output"
64
+ raise ValueError(msg)
65
+
66
+ def interpret(self, probe: Item, frame: Item) -> Item:
67
+ """Return I(probe, frame)."""
68
+ return self.table[(probe, frame)]
69
+
70
+ def require_selected(self, selected: Sequence[Item]) -> None:
71
+ """Reject a selection that names a probe the artifact does not have."""
72
+ probe_set = set(self.probes)
73
+ for probe in selected:
74
+ if probe not in probe_set:
75
+ msg = f"selected probe {probe!r} is not a probe of this artifact"
76
+ raise ValueError(msg)
@@ -0,0 +1,120 @@
1
+ """cases.py - Finite cases mirroring the Lean examples.
2
+
3
+ Each function returns an :class:`Artifact`, using the same carriers and
4
+ interpretation as the corresponding Lean example in
5
+ ``SETest/InterpretiveKernel``. The assertions about these cases live in
6
+ ``check.py``.
7
+
8
+ Lean names are given in each docstring. Carriers of size 2 or 3 use integers;
9
+ Booleans are written 0 and 1.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Sequence
15
+
16
+ from .artifact import Artifact
17
+ from .partition import Item, Partition
18
+
19
+
20
+ def _table(frames, probes, rule):
21
+ """Return the table {(p, f): rule(p, f)}."""
22
+ return {(p, f): rule(p, f) for p in probes for f in frames}
23
+
24
+
25
+ def example_a() -> Artifact:
26
+ """Return Example A: four frames, parity regime, XOR interpretation."""
27
+ frames, probes, outputs = (0, 1, 2, 3), (0, 1), (0, 1)
28
+ return Artifact(
29
+ frames, outputs, probes, _table(frames, probes, lambda p, f: p ^ (f % 2)), "A"
30
+ )
31
+
32
+
33
+ def example_b() -> Artifact:
34
+ """Return Example B: identity interpretation with a single probe."""
35
+ frames, probes, outputs = (0, 1), ("p",), (0, 1)
36
+ return Artifact(
37
+ frames, outputs, probes, _table(frames, probes, lambda _p, f: f), "B"
38
+ )
39
+
40
+
41
+ def example_c() -> Artifact:
42
+ """Return Example C: the interpretation only records whether the frame is 0."""
43
+ frames, probes, outputs = (0, 1, 2), (0, 1), (0, 1)
44
+ return Artifact(
45
+ frames, outputs, probes, _table(frames, probes, lambda _p, f: int(f == 0)), "C"
46
+ )
47
+
48
+
49
+ def example_d() -> Artifact:
50
+ """Return Example D: I(p, f) = p and f, so probe 0 erases the frame."""
51
+ frames, probes, outputs = (0, 1), (0, 1), (0, 1)
52
+ return Artifact(
53
+ frames, outputs, probes, _table(frames, probes, lambda p, f: p & f), "D"
54
+ )
55
+
56
+
57
+ def example_e() -> Artifact:
58
+ """Return Example E: two probes whose images chain 0~1 and 1~2."""
59
+ frames, probes, outputs = (0, 1), (0, 1), (0, 1, 2)
60
+
61
+ def rule(p: int, f: int) -> int:
62
+ return (2 if f else 1) if p else (1 if f else 0)
63
+
64
+ return Artifact(frames, outputs, probes, _table(frames, probes, rule), "E")
65
+
66
+
67
+ def example_f() -> Artifact:
68
+ """Return Example F: one frame, constant output, so the counit is strict."""
69
+ frames, probes, outputs = (0,), ("p",), (0, 1)
70
+ return Artifact(
71
+ frames, outputs, probes, _table(frames, probes, lambda _p, _f: 0), "F"
72
+ )
73
+
74
+
75
+ def example_g() -> Artifact:
76
+ """Return Example G: K need not preserve joins."""
77
+ frames, probes, outputs = (0, 1), ("p",), (0, 1, 2)
78
+ return Artifact(
79
+ frames,
80
+ outputs,
81
+ probes,
82
+ _table(frames, probes, lambda _p, f: 0 if f == 0 else 2),
83
+ "G",
84
+ )
85
+
86
+
87
+ def example_h() -> Artifact:
88
+ """Return Example H: L need not preserve meets."""
89
+ frames, probes, outputs = (0, 1, 2), ("p",), (0, 1)
90
+ return Artifact(
91
+ frames, outputs, probes, _table(frames, probes, lambda _p, f: int(f != 0)), "H"
92
+ )
93
+
94
+
95
+ def example_i() -> Artifact:
96
+ """Return Example I: identity interpretation, incomparable thresholds."""
97
+ frames, probes, outputs = (0, 1, 2), ("p",), (0, 1, 2)
98
+ return Artifact(
99
+ frames, outputs, probes, _table(frames, probes, lambda _p, f: f), "I"
100
+ )
101
+
102
+
103
+ def merge(carrier: Sequence[Item], a: Item, b: Item) -> Partition:
104
+ """Return the partition of ``carrier`` that merges only ``a`` and ``b``."""
105
+ return Partition.from_pairs(carrier, [(a, b)])
106
+
107
+
108
+ def all_cases() -> list[Artifact]:
109
+ """Return every example artifact."""
110
+ return [
111
+ example_a(),
112
+ example_b(),
113
+ example_c(),
114
+ example_d(),
115
+ example_e(),
116
+ example_f(),
117
+ example_g(),
118
+ example_h(),
119
+ example_i(),
120
+ ]
@@ -0,0 +1,293 @@
1
+ """check.py - Exhaustive and randomized checks of the Interpretive Kernel laws.
2
+
3
+ Replaces the SE-210 check. Verifies, on finite artifacts, the statements that
4
+ the Lean development proves in general:
5
+
6
+ - the Galois adjunction L_Q(phi) <= rho iff phi <= K_Q(rho),
7
+ - unit, counit, and the identities L K L = L and K L K = K,
8
+ - monotonicity in the regimes and in the probe family,
9
+ - K preserves meets and L preserves joins,
10
+ - probe-union duality and the empty family,
11
+ - generating-set reduction,
12
+ - agreement of the partition-based code with the literal pair-set oracle,
13
+ - witnesses, thresholds, and pairwise demand,
14
+ - the specific phenomena of Examples A to I.
15
+
16
+ For each artifact, every selected family Q, every phi on the frames, and every
17
+ rho on the outputs is enumerated. This is evidence for small carriers, not a
18
+ proof. The proofs are in the Lean library.
19
+
20
+ Usage::
21
+
22
+ python -m se_interpretive_kernel.check [--random N] [--seed S]
23
+
24
+ Exits nonzero if any check fails.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ from collections.abc import Sequence
31
+ from itertools import combinations
32
+ import sys
33
+
34
+ from .artifact import Artifact
35
+ from .cases import (
36
+ all_cases,
37
+ example_a,
38
+ example_b,
39
+ example_c,
40
+ example_d,
41
+ example_e,
42
+ example_f,
43
+ example_g,
44
+ example_h,
45
+ example_i,
46
+ merge,
47
+ )
48
+ from .demand import demand, kernel_from_demand, ultrametric_violations
49
+ from .generated import generated, generated_all_pairs, threshold
50
+ from .kernel import coherent, kernel
51
+ from .literal import generated_pairs, generator_pairs, kernel_pairs, pairs_of
52
+ from .partition import Item, Partition, all_partitions
53
+ from .sampling import random_artifacts
54
+ from .witness import find_witness, witness_is_valid
55
+
56
+
57
+ def probe_families(artifact: Artifact) -> list[tuple[Item, ...]]:
58
+ """Return every subset of the probes, smallest first, in probe order."""
59
+ subsets: list[tuple[Item, ...]] = []
60
+ for size in range(len(artifact.probes) + 1):
61
+ subsets.extend(combinations(artifact.probes, size))
62
+ return subsets
63
+
64
+
65
+ def check_artifact(artifact: Artifact) -> list[str]:
66
+ """Return a description of every law violated on this artifact."""
67
+ failures: list[str] = []
68
+ name = artifact.label or "artifact"
69
+
70
+ def fail(law: str, detail: str = "") -> None:
71
+ failures.append(f"{name}: {law} {detail}".rstrip())
72
+
73
+ frame_regimes = list(all_partitions(artifact.frames))
74
+ output_regimes = list(all_partitions(artifact.outputs))
75
+ frame_top = Partition.indiscrete(artifact.frames)
76
+ output_bottom = Partition.discrete(artifact.outputs)
77
+ output_top = Partition.indiscrete(artifact.outputs)
78
+ frame_bottom = Partition.discrete(artifact.frames)
79
+ families = probe_families(artifact)
80
+
81
+ for q in families:
82
+ # Totals and bottoms.
83
+ if kernel(artifact, q, output_top) != frame_top:
84
+ fail("K(total) = total", f"Q={q}")
85
+ if generated(artifact, q, frame_bottom) != output_bottom:
86
+ fail("L(discrete) = discrete", f"Q={q}")
87
+
88
+ for phi in frame_regimes:
89
+ lift = generated(artifact, q, phi)
90
+ if lift != generated_all_pairs(artifact, q, phi):
91
+ fail("generating-set reduction", f"Q={q} phi={phi.labels}")
92
+ if pairs_of(lift) != generated_pairs(artifact, q, pairs_of(phi)):
93
+ fail("L matches literal oracle", f"Q={q} phi={phi.labels}")
94
+ if not phi.refines(kernel(artifact, q, lift)):
95
+ fail("unit", f"Q={q} phi={phi.labels}")
96
+ if generated(artifact, q, kernel(artifact, q, lift)) != lift:
97
+ fail("L K L = L", f"Q={q} phi={phi.labels}")
98
+ if threshold(artifact, q, phi) != lift:
99
+ fail("threshold = L", f"Q={q} phi={phi.labels}")
100
+
101
+ for rho in output_regimes:
102
+ ker = kernel(artifact, q, rho)
103
+ if pairs_of(ker) != kernel_pairs(artifact, q, pairs_of(rho)):
104
+ fail("K matches literal oracle", f"Q={q} rho={rho.labels}")
105
+ if not generated(artifact, q, ker).refines(rho):
106
+ fail("counit", f"Q={q} rho={rho.labels}")
107
+ if kernel(artifact, q, generated(artifact, q, ker)) != ker:
108
+ fail("K L K = K", f"Q={q} rho={rho.labels}")
109
+ if kernel_from_demand(artifact, q, rho) != ker:
110
+ fail("kernel via demand", f"Q={q} rho={rho.labels}")
111
+
112
+ for x in artifact.frames:
113
+ for y in artifact.frames:
114
+ if ker.same(x, y) != demand(artifact, q, x, y).refines(rho):
115
+ fail("K iff demand refines rho", f"Q={q} rho={rho.labels}")
116
+
117
+ for phi in frame_regimes:
118
+ adjoint_left = generated(artifact, q, phi).refines(rho)
119
+ adjoint_right = phi.refines(ker)
120
+ if adjoint_left != adjoint_right:
121
+ fail("adjunction", f"Q={q} phi={phi.labels} rho={rho.labels}")
122
+ witness = find_witness(artifact, q, phi, rho)
123
+ if adjoint_right != (witness is None):
124
+ fail(
125
+ "witness iff incoherent",
126
+ f"Q={q} phi={phi.labels} rho={rho.labels}",
127
+ )
128
+ if witness is not None and not witness_is_valid(
129
+ artifact, q, phi, rho, witness
130
+ ):
131
+ fail("witness is valid", f"Q={q} phi={phi.labels} rho={rho.labels}")
132
+ if coherent(artifact, q, phi, rho) != adjoint_right:
133
+ fail("coherent agrees with refinement")
134
+
135
+ # Monotonicity and preservation.
136
+ for r1 in output_regimes:
137
+ for r2 in output_regimes:
138
+ k1, k2 = kernel(artifact, q, r1), kernel(artifact, q, r2)
139
+ if r1.refines(r2) and not k1.refines(k2):
140
+ fail("K monotone")
141
+ if kernel(artifact, q, r1.meet(r2)) != k1.meet(k2):
142
+ fail("K preserves meets", f"Q={q}")
143
+ for p1 in frame_regimes:
144
+ for p2 in frame_regimes:
145
+ l1, l2 = generated(artifact, q, p1), generated(artifact, q, p2)
146
+ if p1.refines(p2) and not l1.refines(l2):
147
+ fail("L monotone")
148
+ if generated(artifact, q, p1.join(p2)) != l1.join(l2):
149
+ fail("L preserves joins", f"Q={q}")
150
+
151
+ # Probe-family laws.
152
+ for q1 in families:
153
+ for q2 in families:
154
+ union = tuple(p for p in artifact.probes if p in q1 or p in q2)
155
+ if set(q1) <= set(q2):
156
+ for rho in output_regimes:
157
+ if not kernel(artifact, q2, rho).refines(kernel(artifact, q1, rho)):
158
+ fail("more probes refine K", f"{q1} <= {q2}")
159
+ for phi in frame_regimes:
160
+ if not generated(artifact, q1, phi).refines(
161
+ generated(artifact, q2, phi)
162
+ ):
163
+ fail("more probes enlarge L", f"{q1} <= {q2}")
164
+ for rho in output_regimes:
165
+ if kernel(artifact, union, rho) != kernel(artifact, q1, rho).meet(
166
+ kernel(artifact, q2, rho)
167
+ ):
168
+ fail("K probe-union duality", f"{q1} {q2}")
169
+ for phi in frame_regimes:
170
+ if generated(artifact, union, phi) != generated(artifact, q1, phi).join(
171
+ generated(artifact, q2, phi)
172
+ ):
173
+ fail("L probe-union duality", f"{q1} {q2}")
174
+
175
+ for q in families:
176
+ for violation in ultrametric_violations(artifact, q):
177
+ fail("ultrametric", f"Q={q} {violation}")
178
+ return failures
179
+
180
+
181
+ def check_cases() -> list[str]:
182
+ """Return failures of the specific phenomena of Examples A to I."""
183
+ failures: list[str] = []
184
+
185
+ def expect(condition: bool, message: str) -> None:
186
+ if not condition:
187
+ failures.append(f"case: {message}")
188
+
189
+ # A: parity regime is coherent with equality on the outputs.
190
+ a = example_a()
191
+ parity = Partition.from_labels(a.frames, {0: 0, 1: 1, 2: 0, 3: 1})
192
+ eq_out = Partition.discrete(a.outputs)
193
+ expect(coherent(a, a.probes, parity, eq_out), "A coherent")
194
+ expect(generated(a, a.probes, parity).refines(eq_out), "A lift finer than equality")
195
+
196
+ # B: the total frame regime is incoherent with equality; the witness is valid.
197
+ b = example_b()
198
+ b_total, b_eq = Partition.indiscrete(b.frames), Partition.discrete(b.outputs)
199
+ expect(not coherent(b, b.probes, b_total, b_eq), "B incoherent")
200
+ w = find_witness(b, b.probes, b_total, b_eq)
201
+ expect(
202
+ w is not None and witness_is_valid(b, b.probes, b_total, b_eq, w), "B witness"
203
+ )
204
+ expect(generated(b, b.probes, b_total).same(0, 1), "B lift relates 0 and 1")
205
+
206
+ # C: the kernel separates 0 from 1 and relates 1 and 2; the unit is strict.
207
+ c = example_c()
208
+ c_eq = Partition.discrete(c.outputs)
209
+ ker_c = kernel(c, c.probes, c_eq)
210
+ expect(ker_c.same(1, 2) and not ker_c.same(0, 1), "C kernel collapses 1 and 2")
211
+ c_frames_eq = Partition.discrete(c.frames)
212
+ expect(
213
+ c_frames_eq != kernel(c, c.probes, generated(c, c.probes, c_frames_eq)),
214
+ "C unit strict",
215
+ )
216
+
217
+ # D: more probes refine the kernel and enlarge the lift, both strictly.
218
+ d = example_d()
219
+ d_eq, d_total = Partition.discrete(d.outputs), Partition.indiscrete(d.frames)
220
+ expect(kernel(d, (0,), d_eq).same(0, 1), "D Q0 does not separate")
221
+ expect(not kernel(d, (0, 1), d_eq).same(0, 1), "D Q1 separates")
222
+ expect(coherent(d, (0,), d_total, d_eq), "D coherent at Q0")
223
+ expect(not coherent(d, (0, 1), d_total, d_eq), "D incoherent at Q1")
224
+ expect(not generated(d, (0,), d_total).same(0, 1), "D L_Q0 stays discrete")
225
+ expect(generated(d, (0, 1), d_total).same(0, 1), "D L_Q1 relates 0 and 1")
226
+
227
+ # E: the generating pairs are not transitive; the closure relates 0 and 2.
228
+ e = example_e()
229
+ e_total = Partition.indiscrete(e.frames)
230
+ expect(generated(e, e.probes, e_total).same(0, 2), "E closure relates 0 and 2")
231
+ expect(
232
+ (0, 2) not in generator_pairs(e, e.probes, pairs_of(e_total)),
233
+ "E generators skip (0, 2)",
234
+ )
235
+
236
+ # F: the counit is strict.
237
+ f = example_f()
238
+ f_total = Partition.indiscrete(f.outputs)
239
+ f_lk = generated(f, f.probes, kernel(f, f.probes, f_total))
240
+ expect(f_lk != f_total and f_lk.refines(f_total), "F counit strict")
241
+
242
+ # G: K does not preserve joins.
243
+ g = example_g()
244
+ rho, sigma = merge(g.outputs, 0, 1), merge(g.outputs, 1, 2)
245
+ kg = lambda r: kernel(g, g.probes, r)
246
+ expect(
247
+ kg(rho.join(sigma)) != kg(rho).join(kg(sigma)), "G K does not preserve joins"
248
+ )
249
+
250
+ # H: L does not preserve meets.
251
+ h = example_h()
252
+ phi1, phi2 = merge(h.frames, 0, 1), merge(h.frames, 0, 2)
253
+ lh = lambda p: generated(h, h.probes, p)
254
+ expect(
255
+ lh(phi1.meet(phi2)) != lh(phi1).meet(lh(phi2)), "H L does not preserve meets"
256
+ )
257
+
258
+ # I: thresholds are incomparable.
259
+ i = example_i()
260
+ t01 = threshold(i, i.probes, merge(i.frames, 0, 1))
261
+ t12 = threshold(i, i.probes, merge(i.frames, 1, 2))
262
+ expect(not t01.refines(t12) and not t12.refines(t01), "I thresholds incomparable")
263
+
264
+ # J: without equivalence the adjunction fails (empty family, empty relation).
265
+ # Partition values are always equivalences, so this is stated, not run.
266
+ return failures
267
+
268
+
269
+ def main(argv: Sequence[str] | None = None) -> int:
270
+ """Run all checks and return a process exit status."""
271
+ parser = argparse.ArgumentParser(description="Check Interpretive Kernel laws.")
272
+ parser.add_argument("--random", type=int, default=50, help="random artifacts")
273
+ parser.add_argument("--seed", type=int, default=0, help="random seed")
274
+ args = parser.parse_args(argv)
275
+
276
+ failures: list[str] = []
277
+ for artifact in all_cases():
278
+ failures.extend(check_artifact(artifact))
279
+ failures.extend(check_cases())
280
+ checked = len(all_cases())
281
+ for artifact in random_artifacts(args.random, args.seed):
282
+ failures.extend(check_artifact(artifact))
283
+ checked += 1
284
+
285
+ for line in failures[:50]:
286
+ print(line)
287
+ status = "FAILED" if failures else "OK"
288
+ print(f"{status}: {checked} artifacts, {len(failures)} failures")
289
+ return 1 if failures else 0
290
+
291
+
292
+ if __name__ == "__main__":
293
+ sys.exit(main())
@@ -0,0 +1,5 @@
1
+ """Command-line interface."""
2
+
3
+ from se_interpretive_kernel.check import main
4
+
5
+ __all__ = ["main"]
@@ -0,0 +1,66 @@
1
+ """demand.py - Pairwise interpretive demand.
2
+
3
+ Definition (Lean: ``demand``). The demand of a pair of frames (x, y) is the
4
+ least occurrence regime under which every selected probe treats x and y alike:
5
+
6
+ delta(x, y) = equivalence closure of {(I(p,x), I(p,y)) | p selected}.
7
+
8
+ Theorem (Lean: ``kernelRel_iff_demand_finer``). For every occurrence regime
9
+ rho, F K_Q(rho) F' exactly when delta(F, F') refines rho.
10
+
11
+ delta is a generalized pseudo-ultrametric: it is the bottom partition on the
12
+ diagonal, symmetric, and satisfies the strong triangle inequality
13
+ delta(x,z) <= delta(x,y) join delta(y,z). Distinct frames may have zero
14
+ demand, so delta does not recover identity of frames.
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, Pair, Partition
23
+
24
+
25
+ def demand(artifact: Artifact, selected: Sequence[Item], x: Item, y: Item) -> Partition:
26
+ """Return the pairwise interpretive demand delta(x, y)."""
27
+ artifact.require_selected(selected)
28
+ pairs: list[Pair] = []
29
+ for probe in selected:
30
+ pairs.append((artifact.interpret(probe, x), artifact.interpret(probe, y)))
31
+ return Partition.from_pairs(artifact.outputs, pairs)
32
+
33
+
34
+ def kernel_from_demand(
35
+ artifact: Artifact, selected: Sequence[Item], rho: Partition
36
+ ) -> Partition:
37
+ """Return K_Q(rho) computed as {(x, y) | delta(x, y) refines rho}."""
38
+ pairs: list[Pair] = []
39
+ for x in artifact.frames:
40
+ for y in artifact.frames:
41
+ if demand(artifact, selected, x, y).refines(rho):
42
+ pairs.append((x, y))
43
+ return Partition.from_pairs(artifact.frames, pairs)
44
+
45
+
46
+ def ultrametric_violations(artifact: Artifact, selected: Sequence[Item]) -> list[str]:
47
+ """Return a description of every violated ultrametric law."""
48
+ bottom = Partition.discrete(artifact.outputs)
49
+ found: list[str] = []
50
+ for x in artifact.frames:
51
+ if demand(artifact, selected, x, x) != bottom:
52
+ found.append(f"zero: demand({x!r}, {x!r}) is not discrete")
53
+ for x in artifact.frames:
54
+ for y in artifact.frames:
55
+ if demand(artifact, selected, x, y) != demand(artifact, selected, y, x):
56
+ found.append(f"symmetry: demand({x!r}, {y!r}) differs from reverse")
57
+ for x in artifact.frames:
58
+ for y in artifact.frames:
59
+ for z in artifact.frames:
60
+ direct = demand(artifact, selected, x, z)
61
+ detour = demand(artifact, selected, x, y).join(
62
+ demand(artifact, selected, y, z)
63
+ )
64
+ if not direct.refines(detour):
65
+ found.append(f"triangle: ({x!r}, {y!r}, {z!r})")
66
+ return found
@@ -0,0 +1,69 @@
1
+ """differences.py - Report where two frame regimes disagree.
2
+
3
+ Given two declared groupings of the same frames, this module lists the groups
4
+ one regime makes that the other regime splits.
5
+
6
+ It reports differences. It does not say which regime is correct, which is
7
+ closer to some truth, or which should be preferred. Deciding that needs
8
+ evidence outside this package.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+
15
+ from .partition import Item, Partition
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class Difference:
20
+ """Contain one group made by one regime and split by the other.
21
+
22
+ ``grouped_by`` is ``"first"`` or ``"second"``: the regime that puts
23
+ ``frames`` in one block. ``split_into`` is how the other regime separates
24
+ those frames, as parts that each stay together under both regimes.
25
+ """
26
+
27
+ grouped_by: str
28
+ frames: tuple[Item, ...]
29
+ split_into: tuple[tuple[Item, ...], ...]
30
+
31
+
32
+ def _splits(block: tuple[Item, ...], common: Partition) -> tuple[tuple[Item, ...], ...]:
33
+ """Return the parts of ``block`` under a partition, in first-seen order."""
34
+ parts: dict[int, list[Item]] = {}
35
+ for frame in block:
36
+ parts.setdefault(common.label_of(frame), []).append(frame)
37
+ return tuple(tuple(part) for part in parts.values())
38
+
39
+
40
+ def regime_differences(first: Partition, second: Partition) -> list[Difference]:
41
+ """Return every group that one regime makes and the other splits.
42
+
43
+ An empty list means the two regimes are the same partition.
44
+ """
45
+ if first.carrier != second.carrier:
46
+ msg = "both regimes must be partitions of the same frames, in order"
47
+ raise ValueError(msg)
48
+ common = first.meet(second)
49
+ found: list[Difference] = []
50
+ for block in first.blocks():
51
+ parts = _splits(block, common)
52
+ if len(parts) > 1:
53
+ found.append(Difference("first", block, parts))
54
+ for block in second.blocks():
55
+ parts = _splits(block, common)
56
+ if len(parts) > 1:
57
+ found.append(Difference("second", block, parts))
58
+ return found
59
+
60
+
61
+ def format_differences(differences: list[Difference]) -> str:
62
+ """Return the differences as plain text, one line each."""
63
+ lines: list[str] = []
64
+ for item in differences:
65
+ parts = " | ".join(", ".join(repr(f) for f in part) for part in item.split_into)
66
+ lines.append(
67
+ f"{item.grouped_by} groups {len(item.frames)} frames; other splits: {parts}"
68
+ )
69
+ return "\n".join(lines)