packages feed

shikumi-optimize-0.3.1.0: src/Shikumi/Optimize/ReAnchor/Fit.hs

-- | Bounded candidates and deterministic held-out selection, without model calls.
module Shikumi.Optimize.ReAnchor.Fit (gaps, tidy, folds, pickBest, selectKept, thresholdCandidates, cutCandidates, weightCandidates) where

import Data.List (maximumBy, nub, sort, sortOn)
import Data.Ord (comparing)
import Data.Word (Word64)
import GHC.Float (castDoubleToWord64, castWord64ToDouble)

tidy :: Double -> Double -> Double -> Double
tidy lo hi midpoint = case [x | digits <- [1 .. 11 :: Int], let scale = 10 ^^ digits, let x = fromInteger (round (midpoint * scale)) / scale, x > lo, x < hi] of
  x : _ -> x
  [] -> midpoint

gaps :: Int -> Bool -> [Double] -> Double -> Double -> [(Double, Double)]
gaps limit geometric values lo hi
  | lo >= hi || (geometric && lo <= 0) = []
  | otherwise = [(tidy a b mid, width a b) | (a, b) <- zip points (drop 1 points), let mid = if geometric then sqrt (a * b) else (a + b) / 2, mid > a, mid < b]
  where
    inner = nub (sort [x | x <- values, x > lo, x < hi])
    count = max 1 (limit - 1)
    thinned
      | length inner <= count = inner
      | count == 1 = [inner !! (length inner `div` 2)]
      | otherwise = nub [inner !! round (fromIntegral i * fromIntegral (length inner - 1) / fromIntegral (count - 1) :: Double) | i <- [0 .. count - 1]]
    points = lo : thinned ++ [hi]
    width a b = if geometric then log (b / a) else b - a

-- | A fixed-seed shuffle followed by round-robin fold assignment. It is stable
-- across runs and independent of candidate order or mutable random state.
folds :: Int -> Int -> [[Int]]
folds requested n = [[x | (i, x) <- zip [0 :: Int ..] shuffled, i `mod` k == j] | j <- [0 .. k - 1]]
  where
    k = min (max 1 requested) n
    shuffled = shuffle (1 :: Word64) [0 .. n - 1]
    shuffle _ [] = []
    shuffle seed xs =
      let seed' = seed * 6364136223846793005 + 1442695040888963407
          ix = fromIntegral (seed' `mod` fromIntegral (length xs))
          (a, b) = splitAt ix xs
       in case b of y : ys -> y : shuffle seed' (a ++ ys); [] -> []

-- | Strict improvement; on equal score and tie key the earliest trial wins.
pickBest :: [Int] -> [Double] -> [([Double], [Double], a)] -> Maybe ([Double], [Double], a)
pickBest rows start tried = case filter (\(scores, _, _) -> total scores > total start) tried of
  [] -> Nothing
  x : xs -> Just (foldl choose x xs)
  where
    total scores = sum [scores !! i | i <- rows]
    key (scores, tie, _) = (total scores, tie)
    choose a b = if key b > key a then b else a

selectKept :: Int -> [Double] -> [([Double], [Double], a)] -> (Maybe ([Double], [Double], a), Bool)
selectKept k start tried = case pickBest rows start tried of
  Nothing -> (Nothing, False)
  Just best
    | held > sum start -> (Just best, False)
    | otherwise -> (Nothing, True)
  where
    rows = [0 .. length start - 1]
    held = sum [sum [scores !! i | i <- fold] | fold <- folds k (length start), let outside = filter (`notElem` fold) rows, let scores = maybe start (\(s, _, _) -> s) (pickBest outside start tried)]

thresholdCandidates :: Int -> [Double] -> Double -> [(Double, [Double])]
thresholdCandidates limit observed start = [(t, [w, negate (abs (t - start)), t]) | (t, w) <- candidates, t /= start]
  where
    sorted = nub (sort observed)
    adjacent = [(b, b - a) | (a, b) <- zip sorted (drop 1 sorted), a >= 0, castWord64ToDouble (castDoubleToWord64 a + 1) == b]
    candidates = gaps limit False observed 0 1 ++ adjacent ++ [(0, 0) | 0 `elem` observed]

cutCandidates :: Int -> [Double] -> Double -> Double -> Double -> [(Double, [Double])]
cutCandidates limit means lo hi current = [(c, [w]) | (c, w) <- gaps limit False means lo hi, c /= current]

weightCandidates :: Int -> [Double] -> Double -> [(Double, [Double])]
weightCandidates limit flips current = [(c, [w]) | (c, w) <- gaps limit True flips low high, c /= current]
  where
    low = case flips of [] -> 1e-3; _ -> max 1e-3 (minimum flips / 4)
    high = case flips of [] -> 1e3; _ -> min 1e3 (maximum flips * 4)