diff --git a/scripts/pipeline_core.py b/scripts/pipeline_core.py index 7e9fcb4c..7119e005 100644 --- a/scripts/pipeline_core.py +++ b/scripts/pipeline_core.py @@ -275,11 +275,18 @@ class COUCHFilter(Filter): class SidonFilter(Filter): """Checks Sidon property via CRT reconstruction. - NOTE: CRT sum-based Sidon check is chiral-invariant (proven — - the negation x→-x is a ring automorphism). To actually discriminate - chiral configs, swap this for DualQuaternionSidonFilter. + POSITIONAL chirality: the chiral config permutes which label goes + to which strand position. Each position has its own modulus. + A permutation is NOT a ring automorphism — different label-to-modulus + mappings CAN produce different Sidon results. - This filter is kept as the default because it's the proven baseline. + The chiral tuple (ε₁, ..., εₖ) is interpreted as: + εⱼ = 0: strand j stays in position j (no swap) + εⱼ = 1: strand j swaps with strand j+1 (positional swap) + + Multiple swaps compose into a full permutation of labels across + positions. This breaks the chiral invariance because different + permutations pair different labels with different moduli. """ @property @@ -289,7 +296,7 @@ class SidonFilter(Filter): def apply(self, configs: list[Config], ctx: PipelineContext) -> list[Config]: result = [] for c in configs: - embedded = self._embed_chiral(c) + embedded = self._embed_chiral_positional(c) collisions = self._sidon_check(embedded, c.moduli) c.collisions = collisions total_pairs = len(c.labels) * (len(c.labels) + 1) // 2 @@ -299,15 +306,35 @@ class SidonFilter(Filter): result.append(c) return result - def _embed_chiral(self, c: Config) -> list[list[int]]: + def _permute_labels(self, labels: tuple, chiral: tuple) -> list: + """Apply positional chirality: chiral[j]=1 swaps positions j and j+1. + + This composes into a full permutation. Multiple swaps can + interact (e.g., swap(0,1) then swap(1,2) moves label 0→2). + """ + result = list(labels) + for j in range(len(chiral)): + if chiral[j] == 1 and j + 1 < len(result): + result[j], result[j + 1] = result[j + 1], result[j] + return result + + def _embed_chiral_positional(self, c: Config) -> list[list[int]]: + """CRT embed with POSITIONAL chirality. + + Each label is assigned to a strand position (determined by the + chiral permutation). Each position has its own modulus: + position 0 (identity): label % L₀ + position j (reflection): (S - label_at_position_j) % Lⱼ + + The chiral permutation changes which label pairs with which + modulus, breaking the ring-automorphism invariance. + """ + permuted = self._permute_labels(c.labels, c.chiral) embedded = [] - for a in c.labels: - row = [a % c.moduli[0]] + for pos, a in enumerate(permuted): + row = [a % c.moduli[0]] # identity axis (shared) for j in range(1, len(c.moduli)): - if c.chiral[j-1] == 0: - row.append((c.S - a) % c.moduli[j]) - else: - row.append((a - c.S) % c.moduli[j]) + row.append((c.S - a) % c.moduli[j]) embedded.append(row) return embedded @@ -347,38 +374,37 @@ class SidonFilter(Filter): # ── Swappable: Dual Quaternion Sidon Filter ─────────────────────────── class DualQuaternionSidonFilter(SidonFilter): - """Sidon filter using dual quaternion products instead of CRT sums. + """Sidon filter using dual quaternion products with POSITIONAL chirality. - Unlike CRT sums (which are chiral-invariant), dual quaternion - products involve quaternion multiplication, which is NOT - negation-invariant. This filter CAN discriminate chiral configs. + The positional permutation changes which label pairs with which + modulus, so the DQ product (which involves r_i·t_j cross terms + with different moduli for different positions) CAN discriminate + chiral configurations. - Dual quaternion: q = r + ε·t where r=rotation, t=translation. - For CRT: r = a mod L0 (identity/poloidal), t = (S-a) mod L1 (reflection/toroidal) - Chiral flip: t → -t (negation of reflection component) - - Product: q_i ⊛ q_j = r_i·r_j + ε·(r_i·t_j + t_i·r_j) - The product's translation part changes under chiral flip because - it involves CROSS terms (r_i·t_j), not just sums. + Unlike the negation-based chiral flip (which is a ring automorphism + and preserves all algebraic structure), the positional permutation + is NOT a ring automorphism and can change the Sidon property. """ @property def name(self) -> str: return "DualQuaternionSidonFilter" - def _embed_chiral(self, c: Config) -> list[list[int]]: - """Embed as [r, t] pairs (dual quaternion components). - r = a mod L0 (rotation/poloidal) - t = (S - a) mod L1 or (a - S) mod L1 (translation/toroidal, chiral) + def _embed_chiral_positional(self, c: Config) -> list[list[int]]: + """Embed as [r, t] pairs with POSITIONAL chirality. + + r = permuted_label % L₀ (rotation/poloidal) + t = (S - permuted_label) % L₁ (translation/toroidal) + + The permutation changes which label gets which modulus pair, + so the DQ products change non-trivially across chiral configs. """ + permuted = self._permute_labels(c.labels, c.chiral) embedded = [] - for a in c.labels: + for a in permuted: r = a % c.moduli[0] if len(c.moduli) > 1: - if c.chiral[0] == 0: - t = (c.S - a) % c.moduli[1] - else: - t = (a - c.S) % c.moduli[1] + t = (c.S - a) % c.moduli[1] else: t = 0 embedded.append([r, t])