Last active
April 30, 2023 00:39
-
-
Save ManuelBlanc/b2fa83a8b6c99ed2fa973d52dd026be5 to your computer and use it in GitHub Desktop.
PRD algorithm constant calculator (Haskell)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- | |
-- Pseudo-random distribution constant calculator. | |
-- | |
-- The constants are calculated by numerically approximating the roots of | |
-- the function f(p) = p - p(c) via Newton-Raphson / bisection. | |
-- Calculating p(c) is easy: evaluate a polynomial and invert it. | |
-- | |
-- For an explanation and visualization of the PRD algorithm, please see | |
-- https://observablehq.com/@manuelblanc/pseudo-random-distribution | |
-- | |
-- This is free and unencumbered software released into the public domain. | |
-- For more information, please refer to <http://unlicense.org/> | |
import Control.Monad (forM_, liftM2) | |
import Text.Printf (printf) | |
config_TOLERANCE = 1e-15 :: Double | |
config_STEPS = 100 :: Int | |
-- Main entrypoint. Outputs a lookup formatted as a Lua table. | |
main :: IO () | |
main = do | |
putStrLn "local prd_constant_lookup = {" | |
forM_ [1 .. config_STEPS-1] $ liftM2 (printf "\t[%d] = %.16f,\n") id calc | |
putStrLn "}" | |
where | |
step_size = recip $ fromIntegral config_STEPS | |
calc i = calculate_c config_TOLERANCE (step_size * fromIntegral i) | |
-- Given a PRD constant c, calculates p(c) and p'(c). | |
nominal_p_and_p' :: RealFrac a => a -> (a, a) | |
nominal_p_and_p' c = (p, p') | |
where | |
p = recip q | |
p' = negate q' * p*p | |
n = subtract 1 $ ceiling $ recip c | |
(q, q') = foldl eval_polynomial (1, 0) $ map fromInteger [n, n-1 .. 1] | |
eval_polynomial (a, a') k = (1 + (1 - k*c)*a, (1 - k*c)*a' - k*a) | |
-- Given a nominal probability and an initial guess `c0`, calculates a better | |
-- approximation of the real value `c = c(p)` using Newton-Raphson. | |
approximate_c :: RealFrac a => a -> a -> a | |
approximate_c p c0 | |
| c1 < 0 = c0 / 2 -- Degrade to bisection if underflowing. | |
| otherwise = c1 | |
where | |
c1 = c0 + step | |
step = (p - p_c) / p_c' | |
(p_c, p_c') = nominal_p_and_p' c0 | |
-- Calculates the PRD constant for a given p, up to the given tolerance. | |
calculate_c tolerance p | |
| p >= 1 = 1 | |
| p <= 0 = 0 | |
| otherwise = c | |
where | |
c = converge initial $ drop 1 $ iterate (approximate_c p) initial | |
initial = p*p | |
converge y (x:xs) | |
| (y - x) < tolerance = x | |
| otherwise = converge x xs |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment