packages feed

hanalyze-0.2.0.0: src/Hanalyze/Optim/Constrained.hs

-- |
-- Module      : Hanalyze.Optim.Constrained
-- Description : 拡張ラグランジュ法による制約付き最適化
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- Constrained optimization via the **Augmented Lagrangian** method.
--
-- Internalizes equality constraints @g_i(x) = 0@ and inequality constraints
-- @h_j(x) ≤ 0@ via Lagrange multipliers + a quadratic penalty, exposing an
-- outer loop that calls an existing unconstrained solver (typically
-- @Hanalyze.Optim.LBFGS@) on each subproblem.
--
-- Augmented Lagrangian:
--
-- @
-- L_A(x, λ, μ, ρ) = f(x)
--                 + Σ_i λ_i g_i(x) + (ρ/2) Σ_i g_i(x)²
--                 + Σ_j (1/(2ρ)) [max(0, μ_j + ρ h_j(x))² - μ_j²]
-- @
--
-- Each outer iteration:
--
--   1. Minimize @L_A@ in @x@ with the inner solver (L-BFGS or Nelder-Mead).
--   2. Update multipliers: @λ ← λ + ρ g(x*)@, @μ ← max(0, μ + ρ h(x*))@.
--   3. Grow the penalty @ρ@ if the constraint violation did not improve.
--
-- Reference: Nocedal & Wright, /Numerical Optimization/, Ch. 17.
module Hanalyze.Optim.Constrained
  ( ConstrainedConfig (..)
  , ConstraintSet (..)
  , defaultConstrainedConfig
  , runAugmentedLagrangian
  , penaltyMethod
  , boxToIneq
  ) where

import qualified Hanalyze.Optim.LBFGS  as LBFGS
import qualified Hanalyze.Optim.Common as OC

-- | A set of constraints.
--
-- Equality constraints:   @g_i(x) = 0@.
-- Inequality constraints: @h_j(x) ≤ 0@.
data ConstraintSet = ConstraintSet
  { csEq   :: ![[Double] -> Double]   -- ^ Equality constraints @g_i@
                                      --   (the satisfying value is 0).
  , csIneq :: ![[Double] -> Double]   -- ^ Inequality constraints @h_j ≤ 0@.
  }

-- | Augmented Lagrangian configuration.
data ConstrainedConfig = ConstrainedConfig
  { ccOuterIter :: !Int                -- ^ Outer iterations (10–30 typical).
  , ccRho0      :: !Double             -- ^ Initial penalty coefficient @ρ₀@.
  , ccRhoGrowth :: !Double             -- ^ Growth rate for @ρ@ (2.0–10.0 typical).
  , ccTolViol   :: !Double             -- ^ Constraint-violation tolerance.
  , ccInnerStop :: !OC.StopCriteria    -- ^ Stop criteria for the inner L-BFGS solver.
  } deriving (Show, Eq)

-- | Default configuration: 20 outer iterations, @ρ₀ = 1.0@, growth 5.0,
-- violation tolerance 1e-6, inner solver capped at 200 iterations.
defaultConstrainedConfig :: ConstrainedConfig
defaultConstrainedConfig = ConstrainedConfig
  { ccOuterIter = 20
  , ccRho0      = 1.0
  , ccRhoGrowth = 5.0
  , ccTolViol   = 1e-6
  , ccInnerStop = OC.defaultStopCriteria { OC.stMaxIter = 200 }
  }

-- | Solve a constrained problem via the Augmented Lagrangian method.
--
-- Returns @(inner solver result, constraint-violation norm)@.
runAugmentedLagrangian
  :: ConstrainedConfig
  -> ([Double] -> Double)        -- ^ Objective (minimized).
  -> ConstraintSet
  -> [Double]                     -- ^ Initial point.
  -> IO (OC.OptimResult, Double)  -- ^ Inner L-BFGS result and violation norm.
runAugmentedLagrangian cfg f cs x0 = do
  let neq    = length (csEq cs)
      nineq  = length (csIneq cs)
      lam0   = replicate neq   0
      mu0    = replicate nineq 0
      rho0   = ccRho0 cfg
  go 0 x0 lam0 mu0 rho0
  where
    go iter x lam mu rho
      | iter >= ccOuterIter cfg = do
          r <- innerSolve x lam mu rho
          return (r, viol (OC.orBest r))
      | otherwise = do
          r <- innerSolve x lam mu rho
          let xNew = OC.orBest r
              vNorm = viol xNew
          if vNorm < ccTolViol cfg
            then return (r, vNorm)
            else do
              -- 乗数更新
              let lamN = zipWith (\l g_i -> l + rho * g_i) lam
                                 [g xNew | g <- csEq cs]
                  muN  = zipWith (\m h_j -> max 0 (m + rho * h_j)) mu
                                 [h xNew | h <- csIneq cs]
                  rhoN = rho * ccRhoGrowth cfg
              go (iter + 1) xNew lamN muN rhoN

    -- 拡張 Lagrangian を内側で最小化
    innerSolve x lam mu rho = do
      let lagrangian xs =
            let fx     = f xs
                eqVals = [g xs | g <- csEq cs]
                inVals = [h xs | h <- csIneq cs]
                eqTerm = sum (zipWith (*) lam eqVals)
                       + (rho / 2) * sum [v * v | v <- eqVals]
                inTerm = sum [ let z = max 0 (m + rho * v)
                               in (z * z - m * m) / (2 * rho)
                             | (m, v) <- zip mu inVals ]
            in fx + eqTerm + inTerm
          lcfg = LBFGS.defaultLBFGSConfig { LBFGS.lbStop = ccInnerStop cfg }
      LBFGS.runLBFGSNumeric lcfg lagrangian x

    -- 制約違反ノルム ||g||² + Σ max(0, h)²
    viol xs =
      let eqV = sum [(g xs)^(2::Int) | g <- csEq cs]
          ineqV = sum [(max 0 (h xs))^(2::Int) | h <- csIneq cs]
      in sqrt (eqV + ineqV)

-- | Expand box constraints (@lo_i ≤ x_i ≤ hi_i@) into two inequality
-- constraints (@≤ 0@) per dimension.
--
-- For each dimension @i@ this emits @lo_i - x_i ≤ 0@ (lower bound) and
-- @x_i - hi_i ≤ 0@ (upper bound). The returned list has length
-- @2 × length bs@.
--
-- @
-- let cs = ConstraintSet { csEq = []
--                        , csIneq = boxToIneq bs ++ otherIneq }
-- (r, viol) <- runAugmentedLagrangian defaultConstrainedConfig f cs x0
-- @
boxToIneq :: OC.Bounds -> [[Double] -> Double]
boxToIneq bs = concat
  [ [ \xs -> lo - (xs !! i)
    , \xs -> (xs !! i) - hi ]
  | (i, (lo, hi)) <- zip [0 ..] bs ]

-- | The simpler **penalty method** — a stripped-down Augmented Lagrangian
-- that omits the multiplier updates and only grows the penalty. Easy to
-- implement and lightweight, but prone to ill-conditioning.
penaltyMethod
  :: ConstrainedConfig
  -> ([Double] -> Double)
  -> ConstraintSet
  -> [Double]
  -> IO (OC.OptimResult, Double)
penaltyMethod cfg f cs x0 = do
  go 0 x0 (ccRho0 cfg)
  where
    go iter x rho
      | iter >= ccOuterIter cfg = do
          r <- innerSolve x rho
          return (r, viol (OC.orBest r))
      | otherwise = do
          r <- innerSolve x rho
          let xNew = OC.orBest r
              vNorm = viol xNew
          if vNorm < ccTolViol cfg
            then return (r, vNorm)
            else go (iter + 1) xNew (rho * ccRhoGrowth cfg)

    innerSolve x rho = do
      let penalty xs =
            let fx     = f xs
                eqV    = sum [(g xs)^(2::Int) | g <- csEq cs]
                ineqV  = sum [(max 0 (h xs))^(2::Int) | h <- csIneq cs]
            in fx + (rho / 2) * (eqV + ineqV)
          lcfg = LBFGS.defaultLBFGSConfig { LBFGS.lbStop = ccInnerStop cfg }
      LBFGS.runLBFGSNumeric lcfg penalty x

    viol xs =
      let eqV = sum [(g xs)^(2::Int) | g <- csEq cs]
          ineqV = sum [(max 0 (h xs))^(2::Int) | h <- csIneq cs]
      in sqrt (eqV + ineqV)