""" pl/core.py — Propagation Logic Core ===================================== P / G → Q The single primitive operator of Propagation Logic. A loaded pattern P = (v_P, L_P) propagates through gradient field G in context C = (Γ_C, θ_C) to produce updated pattern Q. Everything in this module is derived from that operator. Nothing is assumed that is not forced by the mechanism. Key insight (PL v13, Section 2.6): G is not a different kind of thing from P. G is P occupying the gradient role contextually. All G is P post-boundary-imposition. There is no view from outside. This file is also P / G → Q. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union import math # ============================================================================= # LOADED PATTERN # ============================================================================= @dataclass class Pattern: """ P = (v_P, L_P) Definition 2.1 (PL v13): P = (v_P, H_P) with v_P ∈ V, H_P a propagation history. V is the carrier set. L_P = |H_P| ≥ 0 is the informational load. v_P : designation component — what the pattern currently designates. In logic: 0 or 1. In arithmetic: a number. In language: a token or sentence. In physics: a field value. L_P : informational load — magnitude of accumulated propagation history. L_P = 0 : seed state. No history. Propagates freely. L_P > 0 : loaded. Has been through gradient fields. The more it has been through, the harder to propagate further. history : the full qualitative record (L_P is its magnitude). Conceptually rich; computationally we track both. """ v: Any L: float = 0.0 history: List[str] = field(default_factory=list) def __repr__(self) -> str: h = " → ".join(self.history[-3:]) if self.history else "∅" return f"P(v={self.v!r}, L={self.L:.3f}, hist=[{h}])" def __eq__(self, other: object) -> bool: if not isinstance(other, Pattern): return False return self.v == other.v and abs(self.L - other.L) < 1e-9 def __hash__(self) -> int: return hash((repr(self.v), round(self.L, 9))) def is_seed(self) -> bool: """L_P = 0: no propagation history.""" return self.L == 0.0 def copy_with(self, v=None, L=None, history=None) -> "Pattern": return Pattern( v=v if v is not None else self.v, L=L if L is not None else self.L, history=history if history is not None else self.history.copy(), ) def seed(v: Any) -> Pattern: """Convenience: create a seed pattern with no history.""" return Pattern(v=v, L=0.0) # ============================================================================= # CONTEXT # ============================================================================= @dataclass class Context: """ C = (Γ_C, θ_C) Definition 2.2 (PL v13): A context is a pair C = (Γ_C, θ_C) where: Γ_C : the set of gradient fields available in C θ_C : the coherence threshold Key derived quantities: support(P, C) = min(L_P, θ_C) demand(P, C) = max(0, L_P − θ_C) demand = 0 → coherent (pattern is fully supported) demand > 0 → incoherent (pattern needs more context than available) Theorem 2.1 (the propagation rate theorem): Among incoherent patterns, rate ∝ 1/L_P. Simpler patterns propagate faster. This is Zipf's law, natural selection, why e^x is its own derivative — all one theorem. """ gradients: List["Gradient"] theta: float = 1.0 name: str = "C" def support(self, p: Pattern) -> float: """support(P, C) = min(L_P, θ_C)""" return min(p.L, self.theta) def demand(self, p: Pattern) -> float: """demand(P, C) = max(0, L_P − θ_C)""" return max(0.0, p.L - self.theta) def is_coherent(self, p: Pattern) -> bool: """demand = 0: pattern is fully supported by context.""" return self.demand(p) == 0.0 def is_valid(self, p: Pattern, designated: Set = None) -> bool: """ valid = designated AND coherent. designated: values that count as "true" in this carrier. Default: {1, True} (classical logic convention). """ if designated is None: designated = {1, True, 1.0} return p.v in designated and self.is_coherent(p) def propagation_rate(self, p: Pattern) -> float: """ Theorem 2.1: rate = 1/L_P for incoherent patterns. Rate = inf for coherent patterns (already there). """ if self.is_coherent(p): return float("inf") return 1.0 / p.L if p.L > 0 else float("inf") # ============================================================================= # GRADIENT # ============================================================================= class Gradient: """ G — a gradient field. G is P occupying the gradient role contextually (PL v13, Section 2.6). Every G has loaded history. Every G was constituted by prior propagation. G can become P in a higher-order event. The propagation event: P / G → Q v_Q = transform(v_P) — designation changes L_Q = L_P + cost(P) — load accumulates H_Q = H_P + [G.name] — history records this gradient cost: by default 1.0 per propagation step. Zero-cost gradients (identity, observation) can be specified. Variable-cost gradients can depend on P. """ def __init__( self, name: str, transform: Callable[[Any], Any], cost: Union[float, Callable[["Pattern"], float]] = 1.0, domain: Optional[Set] = None, description: str = "", ): self.name = name self._transform = transform self._cost_fn = (cost if callable(cost) else (lambda p, c=cost: c)) self.domain = domain # None = any carrier self.description = description def transform(self, v: Any) -> Any: """Apply the designation transformation.""" return self._transform(v) def cost(self, p: Pattern) -> float: """Load cost for propagating this pattern through this gradient.""" return self._cost_fn(p) def propagate(self, p: Pattern) -> Pattern: """ P / G → Q The primitive operation. Everything else is derived from this. """ v_Q = self._transform(p.v) L_Q = p.L + self._cost_fn(p) hist_Q = p.history + [self.name] return Pattern(v=v_Q, L=L_Q, history=hist_Q) def __call__(self, p: Pattern) -> Pattern: return self.propagate(p) def __repr__(self) -> str: return f"G[{self.name}]" def is_closed_on(self, carrier: Set) -> Tuple[bool, List]: """ Does this gradient keep the carrier closed? Returns (is_closed, violations) where violations = [(v_in, v_out_of_carrier), ...] """ violations = [] for v in carrier: out = self._transform(v) if out not in carrier: violations.append((v, out)) return len(violations) == 0, violations def fixed_points(self, carrier: Set) -> List: """Values v ∈ V where G(v) = v.""" return [v for v in carrier if self._transform(v) == v] def orbit(self, v: Any, max_steps: int = 64) -> List: """ The orbit of v under repeated application of G. Returns the full cycle if found, truncated at max_steps otherwise. If the orbit escapes the carrier (transform raises ValueError), returns the visited list so far — indicating no cycle within V. This is the correct result for closure violations: orbits that exit V have no cycle within V. """ visited = [v] current = v for _ in range(max_steps): try: current = self._transform(current) except (ValueError, KeyError): # Orbit has escaped the carrier — no cycle within V. return visited if current == v: return visited # full cycle visited.append(current) return visited # truncated (no cycle found within max_steps) def cycle_length(self, v: Any, max_steps: int = 64) -> Optional[int]: """ Length of the orbit cycle. None if no cycle found. None is also correct when the orbit escapes the carrier (closure violation). """ o = self.orbit(v, max_steps) if not o: return None current = o[-1] try: next_v = self._transform(current) except (ValueError, KeyError): return None # orbit escapes carrier, no cycle if next_v == o[0]: return len(o) return None # ============================================================================= # GRADIENT FAMILIES # Standard gradient families for known carriers. # Each family is itself a Pattern in a higher-order carrier. # ============================================================================= # ── Classical Logic: V = {0, 1} ──────────────────────────────────────────── def G_neg() -> Gradient: """ Classical negation on {0, 1}. G_neg flips the designation but does NOT change the load. Negating "all dogs bark" costs the same as asserting it. """ return Gradient( name="neg", transform=lambda v: 1 - v, domain={0, 1}, description="Classical negation: v → 1 - v", ) def G_and() -> Gradient: """ Conjunction as a binary gradient. Requires tuple input (v_P, v_Q); returns conjunction value. """ return Gradient( name="and", transform=lambda v: int(v[0] and v[1]) if isinstance(v, tuple) else v, domain=None, description="Classical conjunction", ) def G_or() -> Gradient: """Disjunction.""" return Gradient( name="or", transform=lambda v: int(v[0] or v[1]) if isinstance(v, tuple) else v, domain=None, description="Classical disjunction", ) def G_id() -> Gradient: """ Identity: zero-cost, leaves everything unchanged. The simplest gradient. Fixed point of the gradient-family gradient family. """ return Gradient( name="id", transform=lambda v: v, cost=0.0, description="Identity: v → v, zero cost", ) # ── Fuzzy Logic: V = {0, 0.5, 1} ─────────────────────────────────────────── def G_fuzzy_neg() -> Gradient: """ Fuzzy negation: v → 1 - v. Same formula as classical neg, but on a richer carrier. On V={0,0.5,1}: 0.5 is a fixed point. Excluded middle fails at 0.5. This is DERIVED from the carrier structure, not assumed. """ return Gradient( name="fuzzy_neg", transform=lambda v: 1 - v, domain={0, 0.5, 1}, description="Fuzzy negation: v → 1-v on {0, 0.5, 1}", ) def G_lukasiewicz_and() -> Gradient: """Łukasiewicz conjunction: max(0, v[0] + v[1] - 1).""" return Gradient( name="luk_and", transform=lambda v: max(0, v[0] + v[1] - 1) if isinstance(v, tuple) else v, description="Łukasiewicz conjunction", ) # ── Arithmetic: V = ℕ, ℤ, ℚ, ℝ ───────────────────────────────────────────── def G_succ() -> Gradient: """ Successor: n → n + 1. On ℕ: always closed. No fixed points (nothing maps to itself under +1). """ return Gradient( name="succ", transform=lambda v: v + 1, description="Successor: v → v + 1", ) def G_pred() -> Gradient: """ Predecessor: n → n - 1. On ℕ = {0, 1, 2, ...}: NOT closed (0 → -1 ∉ ℕ). Closure violation FORCES extension to ℤ. This is how ℕ → ℤ is derived, not assumed. """ return Gradient( name="pred", transform=lambda v: v - 1, description="Predecessor: v → v - 1 (forces ℕ→ℤ extension)", ) def G_double() -> Gradient: """v → 2v. On ℤ: closed. Reveals even/odd structure.""" return Gradient( name="double", transform=lambda v: 2 * v, description="Doubling: v → 2v", ) def G_halve() -> Gradient: """ v → v/2. On ℤ: NOT closed for odd integers (1/2 ∉ ℤ). Forces extension to ℚ. This is how ℤ → ℚ is derived. """ return Gradient( name="halve", transform=lambda v: v / 2, description="Halving: v → v/2 (forces ℤ→ℚ extension)", ) def G_sqrt() -> Gradient: """ v → √v. On ℚ⁺: NOT closed (√2 ∉ ℚ). Forces extension to ℝ. This is how ℚ → ℝ is derived. """ return Gradient( name="sqrt", transform=lambda v: v ** 0.5, description="Square root: v → √v (forces ℚ→ℝ extension)", ) def G_neg_sqrt() -> Gradient: """ v → √(-v) for v < 0, else √v. On ℝ: NOT closed for negative values. Forces extension to ℂ. This is how ℝ → ℂ is derived. """ return Gradient( name="neg_sqrt", transform=lambda v: complex(0, (-v)**0.5) if v < 0 else v**0.5, description="Negative sqrt: forces ℝ→ℂ extension", ) # ── Modular arithmetic ────────────────────────────────────────────────────── def G_mod(n: int, op: str = "add1") -> Gradient: """Modular arithmetic on ℤ/nℤ.""" ops = { "add1": lambda v: (v + 1) % n, "add2": lambda v: (v + 2) % n, "neg": lambda v: (-v) % n, "double": lambda v: (2 * v) % n, } if op not in ops: raise ValueError(f"Unknown op {op!r}. Choose from {list(ops)}") return Gradient( name=f"mod{n}_{op}", transform=ops[op], domain=set(range(n)), description=f"Mod-{n} {op}", ) # ── Custom gradient ───────────────────────────────────────────────────────── def G_custom(name: str, mapping: Dict[Any, Any], cost: float = 1.0) -> Gradient: """ Build a gradient from an explicit mapping. Any carrier, any domain. The engine derives what it forces. This is the 'novel carrier' entry point for boundary condition extrapolation. """ def _transform(v: Any) -> Any: if v not in mapping: raise ValueError( f"G[{name}]: value {v!r} not in mapping {set(mapping.keys())}. " f"Carrier extension may be required." ) return mapping[v] return Gradient( name=name, transform=_transform, cost=cost, domain=set(mapping.keys()), description=f"Custom gradient with mapping {mapping}", ) # ============================================================================= # GRADIENT FAMILY REGISTRY # Standard (V, Γ) configurations and what they force. # Used by the engine and the data generator. # ============================================================================= KNOWN_SYSTEMS = { "classical_logic": { "description": "Classical two-valued logic", "carrier": {0, 1}, "gradients": lambda: [G_neg(), G_id()], "designated": {1}, "forced": ["closure", "involution", "excluded_middle", "double_negation"], }, "three_valued_logic": { "description": "Three-valued (Łukasiewicz) logic", "carrier": {0, 0.5, 1}, "gradients": lambda: [G_fuzzy_neg(), G_id()], "designated": {1}, "forced": ["closure", "middle_value_fixed", "excluded_middle_fails"], }, "natural_numbers": { "description": "ℕ with successor (closed) and predecessor (not closed)", "carrier": set(range(10)), # finite sample; full ℕ is unbounded "gradients": lambda: [G_succ(), G_id()], "designated": {1}, "forced": ["closure_under_succ", "no_fixed_points_succ"], }, "integers_forced": { "description": "ℕ ∪ predecessor → forces ℤ", "carrier": {0, 1, 2, 3, 4}, "gradients": lambda: [G_pred(), G_id()], "designated": {1}, "forced": ["closure_violation", "negative_extension_forced"], }, "rationals_forced": { "description": "ℤ ∪ halving → forces ℚ", "carrier": {-2, -1, 0, 1, 2, 3, 4}, "gradients": lambda: [G_halve(), G_id()], "designated": {1}, "forced": ["closure_violation", "rational_extension_forced"], }, "Z4": { "description": "Cyclic group ℤ/4ℤ", "carrier": {0, 1, 2, 3}, "gradients": lambda: [G_mod(4, "add1"), G_id()], "designated": {0}, "forced": ["closure", "uniform_4_cycle", "no_fixed_points"], }, "Z2": { "description": "Cyclic group ℤ/2ℤ (bit flip)", "carrier": {0, 1}, "gradients": lambda: [G_mod(2, "add1"), G_id()], "designated": {0}, "forced": ["closure", "involution"], }, } # ============================================================================= # PROPAGATION CHAIN # Records a sequence of P / G → Q steps. # This is the training unit for the mechanism-first model. # ============================================================================= @dataclass class PropagationChain: """ A recorded sequence of propagation steps. Step 0: P_0 / G_0 → P_1 Step 1: P_1 / G_1 → P_2 ... Step n: P_n / G_n → P_{n+1} This is the fundamental training example: not prose about the mechanism, but the mechanism running. """ steps: List[Tuple[Pattern, Gradient, Pattern]] = field(default_factory=list) context: Optional[Context] = None carrier: Optional[Set] = None def add(self, p_in: Pattern, g: Gradient, p_out: Pattern): self.steps.append((p_in, g, p_out)) def run(self, initial: Pattern, gradients: List[Gradient]) -> "PropagationChain": """Run a chain of propagation steps from initial pattern.""" chain = PropagationChain(context=self.context, carrier=self.carrier) current = initial for g in gradients: next_p = g.propagate(current) chain.add(current, g, next_p) current = next_p return chain def as_text(self) -> str: """ Render as training text. This is the format the LM learns to predict. """ lines = [] if self.carrier: lines.append(f"CARRIER: {sorted(self.carrier, key=str)}") for i, (p_in, g, p_out) in enumerate(self.steps): lines.append( f"STEP {i}: P(v={p_in.v!r}, L={p_in.L:.1f}) / G[{g.name}] " f"→ Q(v={p_out.v!r}, L={p_out.L:.1f})" ) return "\n".join(lines) def demand_profile(self) -> List[float]: """ How does demand change across the chain? Increasing demand: moving away from coherence (incoherence accumulating). Decreasing demand: moving toward coherence (gradient is solving something). """ if not self.context: return [] return [self.context.demand(p_out) for _, _, p_out in self.steps] def __len__(self) -> int: return len(self.steps) def __repr__(self) -> str: return f"Chain({len(self.steps)} steps)"