Current task: —

prime_backend_v2.py

Experiments: none registered · script updated 08-18-2026

No data files registered for this script (utility/diagnostic script).

Source code

Python script (click to expand)
"""prime_backend_v2 — blockwise von Mangoldt cumsum backend (task 2.8ii).

E-016's in-memory sieve materializes C1[m] = sum_{n<=m} LAM(n)/sqrt(n)
and C0[m] = sum LAM(n) log(n)/sqrt(n) for ALL n <= e^20 (~12 GB peak).
a = 12 needs e^24 = 2.65e10 — impossible in memory. This module sieves
blockwise (base primes to sqrt(hi)), keeps only running totals, and
answers cumsum queries at arbitrary m via block-local partial sums.

Usage:
    be = PrimeBackendV2(t_queries)          # t_queries: array of t >= 0
    P = be.prime_sum(t_queries)             # = C1(m)*t - C0(m), m = floor(e^t)-1
validated against E-016's in-memory backend in E-019 (sub-stage A).
"""
import numpy as np


class PrimeBackendV2:
    def __init__(self, t_queries, block_exp=24):
        t = np.asarray(t_queries, dtype=float)
        t = t[t > 0]
        # E-016 convention: sums run over n <= floor(e^t + eps)
        m = np.floor(np.exp(t) + 1e-9).astype(np.int64)
        m = m[m >= 2]
        self.m_sorted = np.unique(m)
        self.hi = int(self.m_sorted[-1]) + 1 if self.m_sorted.size else 2
        self.B = max(1 << 24, int(np.sqrt(self.hi)) + 2)   # >= sqrt(hi)
        self._c1 = np.empty_like(self.m_sorted, dtype=float)
        self._c0 = np.empty_like(self.m_sorted, dtype=float)
        self._built = False

    def _base_primes(self, n):
        sieve = np.ones(n + 1, dtype=bool)
        sieve[:2] = False
        for i in range(2, int(n ** 0.5) + 1):
            if sieve[i]:
                sieve[i * i::i] = False
        return np.flatnonzero(sieve)

    def save_cache(self, path):
        np.savez_compressed(path, m_sorted=self.m_sorted,
                            c1=self._c1, c0=self._c0, hi=self.hi)

    def load_cache(self, path):
        d = np.load(path)
        assert int(d["hi"]) >= self.hi and np.array_equal(
            d["m_sorted"], self.m_sorted), "cache mismatch"
        self._c1, self._c0 = d["c1"], d["c0"]
        self._built = True
        return self

    def build(self, verbose=True):
        """Blockwise sieve: answer C1/C0 at every m in self.m_sorted."""
        hi = self.hi
        B = self.B
        base = self._base_primes(int(np.sqrt(hi)) + 1)
        run1 = 0.0   # running sum LAM/sqrt(n)
        run0 = 0.0   # running sum LAM log(n)/sqrt(n)
        qi = 0
        Q = self.m_sorted
        nblocks = (hi + B - 1) // B
        for bi, lo in enumerate(range(2, hi + 1, B)):
            hib = min(lo + B - 1, hi)
            size = hib - lo + 1
            # exact segmented von Mangoldt:
            # 1) mark composites with a factor p <= sqrt(hib)
            comp = np.zeros(size, dtype=bool)
            for p in base:
                if p * p > hib:
                    break
                first = max(p * p, ((lo + p - 1) // p) * p)
                comp[first - lo::p] = True
            ns64 = np.arange(lo, hib + 1, dtype=np.int64)
            lam = np.zeros(size)
            # 2) survivors are exactly the primes in [lo, hib]
            lam[~comp] = np.log(ns64[~comp])
            # 3) higher prime powers p^k (k>=2) by INDEX (not multiples)
            for p in base:
                if p * p > hib:
                    break
                lp = np.log(p)
                pk = p * p
                while pk <= hib:
                    if pk >= lo:
                        lam[pk - lo] = lp
                    pk *= p
            # cumsums inside block
            ns = ns64.astype(np.float64)
            contrib1 = lam / np.sqrt(ns)
            contrib0 = contrib1 * np.log(ns)
            c1 = run1 + np.cumsum(contrib1)
            c0 = run0 + np.cumsum(contrib0)
            # answer queries inside this block
            while qi < Q.size and Q[qi] <= hib:
                self._c1[qi] = c1[Q[qi] - lo]
                self._c0[qi] = c0[Q[qi] - lo]
                qi += 1
            run1, run0 = float(c1[-1]), float(c0[-1])
            if verbose and (bi % 10 == 0 or bi == nblocks - 1):
                print(f"    block {bi + 1}/{nblocks} (n <= {hib:.3e}) "
                      f"queries answered {qi}/{Q.size}", flush=True)
            if qi >= Q.size:
                break
        self._built = True
        return self

    def cumsums(self, t):
        """C1(m), C0(m) at m = floor(e^t)-1 for array t."""
        assert self._built
        t = np.asarray(t, dtype=float)
        out1 = np.zeros_like(t)
        out0 = np.zeros_like(t)
        pos = t > 0
        m = np.floor(np.exp(t[pos]) + 1e-9).astype(np.int64)
        valid = m >= 2
        idx = np.searchsorted(self.m_sorted, m[valid])
        idx = np.clip(idx, 0, self.m_sorted.size - 1)
        o1 = np.zeros(int(pos.sum()))
        o0 = np.zeros(int(pos.sum()))
        hit = self.m_sorted[idx] == m[valid]
        o1[valid] = np.where(hit, self._c1[idx], np.nan)
        o0[valid] = np.where(hit, self._c0[idx], np.nan)
        if not np.all(hit):
            missing = m[valid][~hit]
            raise ValueError(f"unsieved m values, e.g. {missing[:5]}")
        out1[pos] = o1
        out0[pos] = o0
        return out1, out0

    def prime_sum(self, t):
        """Drop-in replacement for E-016 prime_sum_vec."""
        c1, c0 = self.cumsums(t)
        t = np.asarray(t, dtype=float)
        return c1 * t - c0