SPB Git

spb/metrika Public

Stata-class statistics, GPU-accelerated by Apple Silicon. Native Swift — no Electron, no Python runtime, no compromises.

Swift 92.4% HTML 3.3% R 3% Shell 1.3%
2.8 KB · 78 lines swift
Raw Blame History
1//2//  PhiloxStream.swift3//  Metrika4//5//  Author:  Simon-Pierre Boucher6//  Contact: contact@spboucher.ai7//  Copyright © 2026 Simon-Pierre Boucher. All rights reserved.8//910import Foundation1112/// Sequential random-variate stream over the Philox counter space, for13/// samplers whose draws are inherently ordered (MCMC chains). Unlike the14/// bootstrap's counter-addressable draws, a stream consumes positions one15/// by one — rejection samplers use a variable number — but the sequence16/// is fully determined by (seed, stream id), so chains are reproducible17/// and independent chains never overlap.18///19/// Stream ids occupy bit 62 of the word-counter space plus a 2⁴⁸-word20/// block per id, disjoint from bootstrap draws (low positions) and21/// permutation keys (bit 63).22public struct PhiloxStream: Sendable {23    private let generator: Philox4x3224    private let base: UInt6425    private var position: UInt64 = 02627    public init(seed: UInt64, stream: UInt64 = 0) {28        self.generator = Philox4x32(seed: seed)29        self.base = (UInt64(1) << 62) &+ stream &* (UInt64(1) << 48)30    }3132    /// Uniform in (0, 1) — endpoints excluded so inverse-CDF transforms33    /// and logs stay finite.34    public mutating func nextUniform() -> Double {35        position &+= 136        let u = generator.uniform(at: base &+ position)37        return min(max(u, 5e-324), 1 - 2.2e-16)38    }3940    /// Standard normal via Box–Muller (two uniforms per pair, the spare41    /// is cached).42    private var cachedNormal: Double?43    public mutating func nextNormal() -> Double {44        if let cached = cachedNormal {45            cachedNormal = nil46            return cached47        }48        let u1 = nextUniform()49        let u2 = nextUniform()50        let radius = (-2 * Foundation.log(u1)).squareRoot()51        let angle = 2 * Double.pi * u252        cachedNormal = radius * Foundation.sin(angle)53        return radius * Foundation.cos(angle)54    }5556    /// Gamma(shape, rate) via Marsaglia–Tsang squeeze (with the standard57    /// boost for shape < 1).58    public mutating func nextGamma(shape: Double, rate: Double) -> Double {59        precondition(shape > 0 && rate > 0)60        if shape < 1 {61            // Gamma(a) = Gamma(a+1) · U^(1/a)62            let boosted = nextGamma(shape: shape + 1, rate: rate)63            return boosted * Foundation.pow(nextUniform(), 1 / shape)64        }65        let d = shape - 1.0 / 3.066        let c = 1 / (9 * d).squareRoot()67        while true {68            let z = nextNormal()69            let v = (1 + c * z) * (1 + c * z) * (1 + c * z)70            guard v > 0 else { continue }71            let u = nextUniform()72            if Foundation.log(u) < 0.5 * z * z + d - d * v + d * Foundation.log(v) {73                return d * v / rate74            }75        }76    }77}78