diff --git a/docs/FINITE_INFINITY_DUALITY.md b/docs/FINITE_INFINITY_DUALITY.md new file mode 100644 index 00000000..7637fcf6 --- /dev/null +++ b/docs/FINITE_INFINITY_DUALITY.md @@ -0,0 +1,136 @@ +# Structural Insights — Logarithms and Gödel Boundaries + +## 1. Logarithms Tame Combinatorial Explosion + +**Core insight:** Anything that grows exponentially or combinatorially can be forced through a logarithm to become finite. + +### Examples in SilverSight + +| Domain | Exponential | Log Transform | Result | +|--------|------------|---------------|--------| +| **Fock space** | dim(H_{N,p}) = (N+p-1 choose p) | log(dim) = O(p·log(N)) | Linear in p | +| **BMCTE** | O(K(Np + p·2^p)) | log(cost) = O(log(K) + log(N) + p) | Linear in p | +| **Symbolic regression** | O(expression tree space) | log-log transform | Linear regression | +| **Chaos game** | IFS contraction | log(contraction) = -α·t | Exponential convergence | +| **Eigensolid** | braid crossings | log(crossings) = O(log(steps)) | Logarithmic convergence | + +### Why this matters + +The BMCTE regime is projection-dominated because: +1. The Fock space is exponential in p +2. BMCTE never constructs it — only samples projections +3. The projection operator is logarithmic: log(|Per(U_S)|²) is additive +4. This is why entropy is flat: the projection collapses the exponential + +### Mathematical statement + +For any combinatorial explosion with growth rate f(n): +- If f(n) = O(c^n) → log(f(n)) = O(n) — linear +- If f(n) = O(n!) → log(f(n)) = O(n·log(n)) — linearithmic +- If f(n) = O(n^k) → log(f(n)) = O(k·log(n)) — logarithmic + +**The logarithm is the universal combinatorial compressor.** + +### Connection to "Everything Is Logarithms" + +The paper's core claim: "logarithms are coordinate-free objects; units emerge from ratios." + +This means: +- The logarithm doesn't care about the coordinate system +- It converts multiplicative structure to additive structure +- It converts exponential growth to linear growth +- It's the natural transform for physical laws (most are power laws) + +--- + +## 2. Hachimoji Encoding as Controlled Gödel Explosion + +**Core insight:** The 8-state Hachimoji encoding is a finite boundary on an infinite undecidable space — a "controlled explosion" by Gödel. + +### What Gödel showed + +Gödel's incompleteness theorems: +1. Any sufficiently powerful formal system contains true but unprovable statements +2. The system cannot prove its own consistency +3. The space of all possible statements is infinite and undecidable + +### What Hachimoji does + +The Hachimoji encoding maps infinite equation space to 8 finite states: + +``` +classifyEquation : EquationShape → HachimojiState4D +``` + +Where: +- **Φ** (trivial): fundamental equations (E=mc², a²+b²=c²) +- **Σ** (symmetric): balanced equations +- **Λ** (quantified): equations with bounded quantifiers +- **Π** (complex): high-complexity equations (calculus) +- **Ω** (contradiction): degenerate equations (0=1) +- **Ρ** (tight): high operator count +- **Κ** (marginal): many variables, shallow depth +- **Ζ** (zero): default fallback + +### Why this is a "controlled Gödel explosion" + +1. **The space is infinite:** There are infinitely many possible equations +2. **The encoding is finite:** 8 states, each with a deterministic classifier +3. **The boundary is explicit:** `consistencyInvariant` checks if the classification is consistent +4. **The admission gate:** `admission` returns ADMIT, QUARANTINE, or HOLD + +If extended far enough (to equations that can express their own provability), the Hachimoji encoding would hit Gödel's boundary — it would need to classify statements that are true but unprovable, or consistent but not provably so. + +### The "NaN event" observation + +The user noted: "it is functionally a NaN event if extended far enough" + +This is exactly right. If you try to classify an equation that says "this equation is not classifiable" (a Gödel sentence), the classifier would need to return: +- **Φ** (trivial) — but it's not trivial, it's self-referential +- **Ω** (contradiction) — but it's not a contradiction, it's true +- **Ζ** (fallback) — but this is a cop-out, not a classification + +The 8 states form a **finite boundary** on an infinite undecidable space. This is the Gödel explosion, controlled by the finite alphabet. + +### Connection to BMCTE + +The BMCTE regime is projection-dominated because: +1. The Fock space is exponential in p (combinatorial explosion) +2. BMCTE never constructs it — only samples projections (logarithmic compression) +3. The Hachimoji encoding bounds the undecidable (Gödel explosion, controlled) + +Both are examples of **SilverSight's core principle: tame infinity with finite structure.** + +### Mathematical statement + +For any formal system S with Gödel number G(S): +- G(S) grows without bound as S becomes more powerful +- Hachimoji encodes G(S) into 8 finite states +- This is a lossy compression: some Gödel sentences map to Ζ (fallback) +- But it's a **controlled** lossy compression: the admission gate decides what's ADMIT vs QUARANTINE + +**The Hachimoji encoding is a finite Gödel boundary.** + +--- + +## 3. Unifying Principle + +Both insights share the same structure: + +| | Exponential | Finite Boundary | +|---|------------|-----------------| +| **Logarithm** | Combinatorial growth | Logarithmic compression | +| **Hachimoji** | Infinite equation space | 8-state encoding | +| **BMCTE** | Fock space | Projection sampling | +| **Chaos game** | Expression tree space | IFS contraction | + +**SilverSight's core principle: tame infinity with finite structure.** + +This is why the system works: +- It never constructs the full space (exponential, infinite, undecidable) +- It only samples projections (logarithmic, finite, decidable) +- The projections are enough to discover the laws (Kepler, Newton, etc.) + +**The logarithm is the universal combinatorial compressor. +The Hachimoji encoding is the universal Gödel boundary. +Together, they form SilverSight's finite-infinity duality.** diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index 90ca031a..be84942e 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -157,6 +157,19 @@ name and explains the delta. | **ContinuousInterpolation** | λ(p) = exp(-p²/N) smooth blending parameter between fully bosonic (λ→1) and weakly-correlated (λ→0) regimes. | `experiments/bosonic_continuous/tests/test_lambda_interpolation.py` | | **EntropyInvarianceHypothesis** | In BMCTE regime, Shannon entropy H(p) ≈ 10 bits remains constant across p=1..6 for N=2000, indicating projection-dominated measurement rather than full-state exploration. | `experiments/bosonic_continuous/README.md` | +## Symbolic Regression (added 2026-06-22) + +| Term | Definition | Source module | +|------|------------|---------------| +| **ExprNode** | Expression tree node: leaf (variable 'x' or constant) or internal (unary/binary operator). Supports evaluate, to_string, complexity, copy. | `python/expr_tree.py` | +| **Linear Scaling** | Keijzer 2003 technique: GP searches for shape g(x), closed-form solves f(x) = a·g(x) + b via least squares. Reduces search space dramatically. | `python/linear_scaling.py` | +| **BIC Fitness** | Bayesian Information Criterion: `n·ln(MSE) + k·ln(n)` where k = weighted complexity (var=1, const=3). Penalizes overfitting. | `python/expr_tree.py` | +| **Chaos Game Search** | IFS contraction over expression space: at each step, try to improve best expression by applying transformations. Deterministic (same input → same output). | `python/expr_tree.py` | +| **Hachimoji-Guided Search** | Use classifyEquation to partition expressions into 8 Hachimoji states (Φ=trivial, Σ=symmetric, Λ=quantified, Π=complex, Ω=contradiction). Force diversity across states. | `python/hachimoji_citation.py` | +| **Log Prescreen** | Before expression tree search, try log-log, log-y, y-x transforms. If R² > 0.999, return as discovered law. Collapses O(tree search) to O(linear regression) for power laws, exponentials, polynomials. | `python/log_prescreen.py` | +| **Finite-Infinity Duality** | SilverSight's core principle: tame infinity with finite structure. Logarithms compress combinatorial explosion; Hachimoji encoding bounds Gödel's undecidable. Both are lossy compressions that preserve enough structure to discover physical laws. | `docs/FINITE_INFINITY_DUALITY.md` | +| **Gödel Boundary** | The 8-state Hachimoji encoding is a finite boundary on infinite undecidable equation space. If extended to self-referential equations (Gödel sentences), it would need to classify "this equation is not classifiable" — a NaN event. The admission gate (ADMIT/QUARANTINE/HOLD) controls this boundary. | `formal/CoreFormalism/HachimojiCodec.lean` | + ## Modules and library layers | Term | Definition | Source module | diff --git a/python/log_prescreen.py b/python/log_prescreen.py new file mode 100644 index 00000000..900caf82 --- /dev/null +++ b/python/log_prescreen.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +""" +log_prescreen.py — Logarithmic Pre-screening for Symbolic Regression + +"Everything Is Logarithms" — multiplicative structure becomes additive under log. + +Before searching expression trees, try these transforms: + 1. log-log: log(y) vs log(x) → power law y = a·x^b + 2. log-y: log(y) vs x → exponential y = e^(a·x + c) + 3. y-x: y vs x → polynomial y = a·x + b + 4. y-vs-x²: y vs x² → quadratic y = a·x² + b + 5. y-vs-√x: y vs √x → sqrt law y = a·√x + b + +If any transform gives R² > threshold, return that as the discovered law. +This collapses O(expression tree search) to O(linear regression) for +the most common physical laws. +""" + +from __future__ import annotations + +import math +import numpy as np +from dataclasses import dataclass +from typing import Optional, Tuple + + +@dataclass +class PrescreenResult: + """Result from log prescreening.""" + name: str # "power_law", "exponential", "polynomial", etc. + expression: str # human-readable: "y = 1.000 * x^1.500" + r2: float # R² in original space + r2_transform: float # R² in transformed space + params: Tuple[float, ...] # (a, b, ...) coefficients + transform: str # "log-log", "log-y", "y-x", etc. + + +def _linear_r2(x: np.ndarray, y: np.ndarray) -> Tuple[float, float, float]: + """Linear regression y = a·x + b. Returns (a, b, R²).""" + if len(x) < 2: + return 0.0, 0.0, 0.0 + coeffs = np.polyfit(x, y, 1) + a, b = coeffs + y_pred = a * x + b + ss_res = np.sum((y - y_pred) ** 2) + ss_tot = np.sum((y - np.mean(y)) ** 2) + r2 = 1.0 - ss_res / ss_tot if ss_tot > 1e-15 else 0.0 + return float(a), float(b), float(r2) + + +def _multivar_r2(X: np.ndarray, y: np.ndarray) -> Tuple[np.ndarray, float]: + """Multiple linear regression y = X·coeffs. Returns (coeffs, R²).""" + if X.shape[0] < X.shape[1] + 1: + return np.zeros(X.shape[1]), 0.0 + # Least squares: coeffs = (X^T X)^-1 X^T y + try: + coeffs = np.linalg.lstsq(X, y, rcond=None)[0] + y_pred = X @ coeffs + ss_res = np.sum((y - y_pred) ** 2) + ss_tot = np.sum((y - np.mean(y)) ** 2) + r2 = 1.0 - ss_res / ss_tot if ss_tot > 1e-15 else 0.0 + return coeffs, float(r2) + except np.linalg.LinAlgError: + return np.zeros(X.shape[1]), 0.0 + + +def prescreen( + x: np.ndarray, + y: np.ndarray, + feature_names: list[str] = None, + r2_threshold: float = 0.999, +) -> Optional[PrescreenResult]: + """Try log transforms before expression tree search. + + Returns PrescreenResult if any transform gives R² > threshold. + Returns None if no simple law found (caller should use full search). + + Args: + x: input array (n,) or (n, d) for multivariate + y: target array (n,) + feature_names: optional names for features + r2_threshold: minimum R² to accept (default 0.999) + + Returns: + PrescreenResult or None + """ + x = np.asarray(x, dtype=np.float64) + y = np.asarray(y, dtype=np.float64) + + if feature_names is None: + if x.ndim == 1: + feature_names = ["x"] + else: + feature_names = [f"x{i}" for i in range(x.shape[1])] + + # Ensure 2D + if x.ndim == 1: + x = x.reshape(-1, 1) + + n = len(y) + if n < 3: + return None + + results = [] + + # ── 0. Linear: y = a·x + b (CHECK FIRST — power law with exp=1 is also linear) ── + for j, name in enumerate(feature_names): + a, b, r2 = _linear_r2(x[:, j], y) + if r2 > r2_threshold: + results.append(PrescreenResult( + name="linear", + expression=f"{a:.6f}*{name} + {b:.6f}", + r2=r2, + r2_transform=r2, + params=(a, b), + transform="y-x", + )) + + # ── 1. Power law: y = a·x^b (log-log: log(y) = b·log(x) + log(a)) ── + if np.all(x > 0) and np.all(y > 0): + log_x = np.log(x) + log_y = np.log(y) + for j, name in enumerate(feature_names): + a, b, r2 = _linear_r2(log_x[:, j], log_y) + if r2 > r2_threshold: + # y = e^b · x^a (a is slope, b is intercept) + coeff = np.exp(b) + exponent = a + y_pred = coeff * x[:, j] ** exponent + r2_orig = 1 - np.sum((y - y_pred)**2) / np.sum((y - np.mean(y))**2) + results.append(PrescreenResult( + name="power_law", + expression=f"{coeff:.6f} * {name}^{exponent:.6f}", + r2=float(r2_orig), + r2_transform=r2, + params=(coeff, exponent), + transform="log-log", + )) + + # ── 2. Exponential: y = e^(a·x + c) (log-y: log(y) = a·x + c) ── + if np.all(y > 0): + log_y = np.log(y) + for j, name in enumerate(feature_names): + a, c, r2 = _linear_r2(x[:, j], log_y) + if r2 > r2_threshold: + y_pred = np.exp(a * x[:, j] + c) + r2_orig = 1 - np.sum((y - y_pred)**2) / np.sum((y - np.mean(y))**2) + results.append(PrescreenResult( + name="exponential", + expression=f"exp({a:.6f}*{name} + {c:.6f})", + r2=float(r2_orig), + r2_transform=r2, + params=(a, c), + transform="log-y", + )) + + # ── 4. Quadratic: y = a·x² + b·x + c ── + for j, name in enumerate(feature_names): + X_quad = np.column_stack([x[:, j]**2, x[:, j], np.ones(n)]) + coeffs, r2 = _multivar_r2(X_quad, y) + if r2 > r2_threshold: + a, b, c = coeffs + results.append(PrescreenResult( + name="quadratic", + expression=f"{a:.6f}*{name}² + {b:.6f}*{name} + {c:.6f}", + r2=r2, + r2_transform=r2, + params=(a, b, c), + transform="y-x²", + )) + + # ── 5. Sqrt: y = a·√x + b ── + if np.all(x >= 0): + for j, name in enumerate(feature_names): + sqrt_x = np.sqrt(x[:, j]) + a, b, r2 = _linear_r2(sqrt_x, y) + if r2 > r2_threshold: + results.append(PrescreenResult( + name="sqrt_law", + expression=f"{a:.6f}*sqrt({name}) + {b:.6f}", + r2=r2, + r2_transform=r2, + params=(a, b), + transform="y-√x", + )) + + # ── 6. Multivariate power law: y = a·x₁^b₁·x₂^b₂·... ── + if x.shape[1] > 1 and np.all(x > 0) and np.all(y > 0): + log_x_all = np.log(x) + log_y = np.log(y) + X_log = np.column_stack([log_x_all, np.ones(n)]) + coeffs, r2 = _multivar_r2(X_log, log_y) + if r2 > r2_threshold: + exponents = coeffs[:-1] + intercept = coeffs[-1] + coeff = np.exp(intercept) + terms = [f"{name}^{e:.4f}" for name, e in zip(feature_names, exponents)] + expression = f"{coeff:.6f} * " + " * ".join(terms) + results.append(PrescreenResult( + name="multivariate_power_law", + expression=expression, + r2=r2, + r2_transform=r2, + params=tuple(coeffs), + transform="log-log-multivar", + )) + + # Return best result + if not results: + return None + + best = max(results, key=lambda r: r.r2) + if best.r2 >= r2_threshold: + return best + return None + + +def prescreen_or_search( + x: np.ndarray, + y: np.ndarray, + feature_names: list[str] = None, + r2_threshold: float = 0.999, +) -> Tuple[str, float, str]: + """Try prescreen first, return (expression, r2, method). + + If prescreen fails, returns ("", 0.0, "none") — caller should use full search. + """ + result = prescreen(x, y, feature_names, r2_threshold) + if result is not None: + return result.expression, result.r2, result.name + return "", 0.0, "none" diff --git a/tests/test_log_prescreen.py b/tests/test_log_prescreen.py new file mode 100644 index 00000000..5312a6cc --- /dev/null +++ b/tests/test_log_prescreen.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +""" +test_log_prescreen.py — Test log prescreening on known physical laws + +Verifies that the "Everything Is Logarithms" pre-filter discovers +common physical laws without expression tree search. +""" + +import sys +import numpy as np + +sys.path.insert(0, "/home/allaun/SilverSight/python") +from log_prescreen import prescreen + + +def test_kepler(): + """Kepler's Third Law: T = a^1.5""" + a = np.array([0.387, 0.723, 1.000, 1.524, 5.203, 9.537, 19.191, 30.069]) + T = np.array([0.241, 0.615, 1.000, 1.881, 11.862, 29.457, 84.011, 164.79]) + result = prescreen(a, T, feature_names=["a"]) + assert result is not None, "Kepler: prescreen failed" + assert result.r2 > 0.999, f"Kepler: R² = {result.r2:.6f} < 0.999" + assert "power_law" in result.name, f"Kepler: expected power_law, got {result.name}" + coeff, exp = result.params + assert abs(exp - 1.5) < 0.01, f"Kepler: exponent = {exp:.4f}, expected 1.5" + print(f" ✓ Kepler: {result.expression} R²={result.r2:.10f}") + return True + + +def test_hooke(): + """Hooke's Law: F = k·x (linear)""" + x = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]) + F = np.array([2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0]) # k=20 + result = prescreen(x, F, feature_names=["x"]) + assert result is not None, "Hooke: prescreen failed" + assert result.r2 > 0.999, f"Hooke: R² = {result.r2:.6f} < 0.999" + assert "linear" in result.name, f"Hooke: expected linear, got {result.name}" + print(f" ✓ Hooke: {result.expression} R²={result.r2:.10f}") + return True + + +def test_newton_gravity(): + """Newton's inverse square law: F = G·m₁·m₂/r² (power law with exp=-2)""" + r = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]) + F = np.array([100.0, 25.0, 11.11, 6.25, 4.0, 2.78, 2.04, 1.5625]) # F = 100/r² + result = prescreen(r, F, feature_names=["r"]) + assert result is not None, "Newton: prescreen failed" + assert result.r2 > 0.999, f"Newton: R² = {result.r2:.6f} < 0.999" + assert "power_law" in result.name, f"Newton: expected power_law, got {result.name}" + coeff, exp = result.params + assert abs(exp - (-2.0)) < 0.01, f"Newton: exponent = {exp:.4f}, expected -2.0" + print(f" ✓ Newton: {result.expression} R²={result.r2:.10f}") + return True + + +def test_radioactive_decay(): + """Radioactive decay: N = N₀·exp(-λt) (exponential)""" + t = np.array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]) + N = 100.0 * np.exp(-0.3 * t) + result = prescreen(t, N, feature_names=["t"]) + assert result is not None, "Decay: prescreen failed" + assert result.r2 > 0.999, f"Decay: R² = {result.r2:.6f} < 0.999" + assert "exponential" in result.name, f"Decay: expected exponential, got {result.name}" + lambda_est, N0_log = result.params + assert abs(lambda_est - (-0.3)) < 0.01, f"Decay: λ = {lambda_est:.4f}, expected -0.3" + print(f" ✓ Decay: {result.expression} R²={result.r2:.10f}") + return True + + +def test_free_fall(): + """Free fall: s = ½gt² (quadratic with no linear term)""" + t = np.array([0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0]) + g = 9.81 + s = 0.5 * g * t**2 + result = prescreen(t, s, feature_names=["t"]) + assert result is not None, "Free fall: prescreen failed" + assert result.r2 > 0.999, f"Free fall: R² = {result.r2:.6f} < 0.999" + print(f" ✓ Free fall: {result.expression} R²={result.r2:.10f}") + return True + + +def test_sqrt_law(): + """Pendulum period: T = 2π·√(L/g) ≈ a·√L""" + L = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]) + T = 2 * np.pi * np.sqrt(L / 9.81) # T = 2π√(L/g) + result = prescreen(L, T, feature_names=["L"]) + assert result is not None, "Pendulum: prescreen failed" + assert result.r2 > 0.999, f"Pendulum: R² = {result.r2:.6f} < 0.999" + print(f" ✓ Pendulum: {result.expression} R²={result.r2:.10f}") + return True + + +def test_stefan_boltzmann(): + """Stefan-Boltzmann: P = σ·T⁴ (power law with exp=4)""" + T = np.array([100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 700.0, 800.0, 900.0, 1000.0]) + sigma = 5.67e-8 + P = sigma * T**4 + result = prescreen(T, P, feature_names=["T"]) + assert result is not None, "Stefan-Boltzmann: prescreen failed" + assert result.r2 > 0.999, f"Stefan-Boltzmann: R² = {result.r2:.6f} < 0.999" + assert "power_law" in result.name, f"Stefan-Boltzmann: expected power_law, got {result.name}" + coeff, exp = result.params + assert abs(exp - 4.0) < 0.01, f"Stefan-Boltzmann: exponent = {exp:.4f}, expected 4.0" + print(f" ✓ Stefan-Boltzmann: {result.expression} R²={result.r2:.10f}") + return True + + +def test_no_simple_law(): + """sin(x) — should NOT be caught by prescreen (require expression tree search)""" + x = np.linspace(0, 2 * np.pi, 20) + y = np.sin(x) + result = prescreen(x, y, feature_names=["x"], r2_threshold=0.999) + if result is None: + print(f" ✓ sin(x): correctly not detected (requires expression tree search)") + else: + print(f" ~ sin(x): detected as {result.name} (R²={result.r2:.6f})") + return True + + +def main(): + print("=" * 60) + print("SilverSight Log Prescreen — Physical Law Tests") + print("=" * 60) + print() + + tests = [ + ("Kepler's Third Law (T = a^1.5)", test_kepler), + ("Hooke's Law (F = kx)", test_hooke), + ("Newton Gravity (F = 1/r²)", test_newton_gravity), + ("Radioactive Decay (N = N₀e^(-λt))", test_radioactive_decay), + ("Free Fall (s = ½gt²)", test_free_fall), + ("Pendulum Period (T ∝ √L)", test_sqrt_law), + ("Stefan-Boltzmann (P ∝ T⁴)", test_stefan_boltzmann), + ("sin(x) — NOT a simple law", test_no_simple_law), + ] + + passed = 0 + failed = 0 + for name, test_fn in tests: + try: + result = test_fn() + if result: + passed += 1 + else: + failed += 1 + print(f" ✗ {name}") + except Exception as e: + failed += 1 + print(f" ✗ {name}: {e}") + + print() + print("=" * 60) + print(f"Results: {passed} passed, {failed} failed out of {len(tests)}") + print("=" * 60) + + return 0 if failed == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file