hanalyze-0.2.0.0: src/Hanalyze/Optim/ParticleSwarm.hs
-- |
-- Module : Hanalyze.Optim.ParticleSwarm
-- Description : Particle Swarm Optimization (PSO) — Kennedy & Eberhart 1995
-- Copyright : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License : BSD-3-Clause
--
-- Particle Swarm Optimization (PSO).
--
-- Kennedy & Eberhart (1995). A metaheuristic in which a swarm of particles
-- updates velocity by being attracted to its personal best (pbest) and the
-- global best (gbest).
--
-- Velocity / position update:
--
-- @
-- v_{t+1} = w · v_t + c_1 · r_1 · (pbest - x) + c_2 · r_2 · (gbest - x)
-- x_{t+1} = x_t + v_{t+1}
-- @
--
-- Here @w@ is inertia, @c_1@ the cognitive coefficient, @c_2@ the social
-- coefficient, and @r_1, r_2 ~ U(0, 1)@.
{-# LANGUAGE StrictData #-}
module Hanalyze.Optim.ParticleSwarm
( PSOConfig (..)
, defaultPSOConfig
, runPSO
, runPSOWith
) where
import Control.Monad (forM, replicateM)
import Data.List (minimumBy)
import Data.Ord (comparing)
import Data.IORef
import qualified System.Random.MWC as MWC
import Hanalyze.Optim.Common
-- | PSO configuration.
data PSOConfig = PSOConfig
{ psoStop :: !StopCriteria
, psoNum :: !Int -- ^ Number of particles (20–50 typical).
, psoInertia :: !Double -- ^ Inertia @w@ (0.4–0.9 typical).
, psoCog :: !Double -- ^ Cognitive coefficient @c₁@ (1.5–2.0 typical).
, psoSoc :: !Double -- ^ Social coefficient @c₂@ (1.5–2.0 typical).
, psoBounds :: !Bounds -- ^ Per-dimension bounds.
, psoVMax :: !Double -- ^ Velocity cap as a fraction of the
-- range per dimension (e.g. 0.5).
, psoDir :: !Direction
} deriving (Show, Eq)
-- | Default configuration: 200 iterations, swarm size @max(20, 5×D)@,
-- @w = 0.7@, @c₁ = c₂ = 1.5@, @vMax = 0.5@.
defaultPSOConfig :: [(Double, Double)] -> PSOConfig
defaultPSOConfig bs = PSOConfig
{ psoStop = defaultStopCriteria { stMaxIter = 200 }
, psoNum = max 20 (5 * length bs)
, psoInertia = 0.7
, psoCog = 1.5
, psoSoc = 1.5
, psoBounds = bs
, psoVMax = 0.5
, psoDir = Minimize
}
-- | Run PSO with the default configuration built from @bounds@.
runPSO :: [(Double, Double)]
-> ([Double] -> Double)
-> MWC.GenIO
-> IO OptimResult
runPSO bs f gen = runPSOWith (defaultPSOConfig bs) f gen
-- | Run PSO with a user-specified configuration.
runPSOWith :: PSOConfig
-> ([Double] -> Double)
-> MWC.GenIO
-> IO OptimResult
runPSOWith cfg fUser gen = do
let f = flipFor (psoDir cfg) fUser
bs = psoBounds cfg
n = length bs
np = psoNum cfg
vMaxes = [ psoVMax cfg * (hi - lo) | (lo, hi) <- bs ]
-- 初期化
xs0 <- replicateM np (sampleUniformIn bs gen)
vs0 <- replicateM np $ forM (zip bs vMaxes) $ \((lo, hi), vM) -> do
u <- MWC.uniformR (-1, 1) gen
return ((u :: Double) * vM * 0.1)
let fs0 = map f xs0
posRef <- newIORef xs0
velRef <- newIORef vs0
pbestRef <- newIORef (zip xs0 fs0)
gbestRef <- newIORef (minimumBy (comparing snd) (zip xs0 fs0))
histRef <- newIORef [snd (minimumBy (comparing snd) (zip xs0 fs0))]
iterRef <- newIORef 0
let stop = psoStop cfg
maxI = stMaxIter stop
let loop = do
i <- readIORef iterRef
if i >= maxI then return ()
else do
xs <- readIORef posRef
vs <- readIORef velRef
pb <- readIORef pbestRef
(gbX, gbF) <- readIORef gbestRef
-- 更新
updated <- forM (zip3 xs vs pb) $ \(x, v, (px, pf)) -> do
vNew <- forM (zip4 x v px gbX) $ \(xi, vi, pxi, gxi) -> do
r1 <- MWC.uniformR (0, 1) gen :: IO Double
r2 <- MWC.uniformR (0, 1) gen :: IO Double
pure $ psoInertia cfg * vi
+ psoCog cfg * r1 * (pxi - xi)
+ psoSoc cfg * r2 * (gxi - xi)
-- vMax クリップ
let vClipped = zipWith (\vi vM -> max (-vM) (min vM vi)) vNew vMaxes
-- 位置更新 + bounds 反射
let xNew = clipToBounds bs (zipWith (+) x vClipped)
let fNew = f xNew
-- pbest 更新
let (pxN, pfN) = if fNew < pf then (xNew, fNew) else (px, pf)
return (xNew, vClipped, (pxN, pfN), fNew)
let xsN = [a | (a, _, _, _) <- updated]
vsN = [b | (_, b, _, _) <- updated]
pbN = [c | (_, _, c, _) <- updated]
bestC = minimumBy (comparing snd) [(a, d) | (a, _, _, d) <- updated]
(gbXN, gbFN) = if snd bestC < gbF then bestC else (gbX, gbF)
writeIORef posRef xsN
writeIORef velRef vsN
writeIORef pbestRef pbN
writeIORef gbestRef (gbXN, gbFN)
modifyIORef histRef (gbFN :)
writeIORef iterRef (i + 1)
loop
loop
(gbX, gbF) <- readIORef gbestRef
iters <- readIORef iterRef
histR <- readIORef histRef
let vUser = case psoDir cfg of { Minimize -> gbF; Maximize -> negate gbF }
hU = case psoDir cfg of
Minimize -> reverse histR
Maximize -> map negate (reverse histR)
return $ OptimResult gbX vUser hU iters False
where
zip4 (a:as) (b:bs) (c:cs) (d:ds) = (a, b, c, d) : zip4 as bs cs ds
zip4 _ _ _ _ = []