packages feed

hanalyze-design-0.2.0.1: src/Hanalyze/Design/Constraint.hs

{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module      : Hanalyze.Design.Constraint
-- Description : DoE 古典側の設計制約 (線形不等式・禁止行の組合せ) によるフィルタ / 検証
-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License     : BSD-3-Clause
--
-- [日本語]: DoE 古典側の設計制約。
--
-- 候補集合ベースの 'Hanalyze.Design.Optimal' に渡す前のフィルタ用途、
-- および手動構築した設計行列の事後検証用途を想定。
--
-- ADT は最小 2 種:
--
--   - 'LinearConstraint' coeffs rel rhs — 線形不等式 / 等式
--     @sum_i (coeffs[i] * x[i]) @rel@ rhs@
--   - 'ForbiddenCombination' values — 厳密に一致する row を禁止
--     (浮動小数比較は 'forbiddenTolerance' = 1e-9 で許容)
--
-- 条件付 (If-then) 制約は本モジュールでは扱わない (Custom Design spec 専有、
-- spec/hanalyze-doe-custom-design-spec.md §2.3 / §9 参照)。
--
-- spec: doe-spec v0.2 §2.8 / §3.12。
--
-- [English]: Design constraints on the classical DoE side.
--
-- Intended for use as a filter before candidate sets are passed to
-- 'Hanalyze.Design.Optimal', and for post-hoc verification of
-- manually constructed design matrices.
--
-- The ADT has a minimal 2 variants:
--
--   - 'LinearConstraint' coeffs rel rhs — linear inequality / equality
--     @sum_i (coeffs[i] * x[i]) @rel@ rhs@
--   - 'ForbiddenCombination' values — forbids a row that matches exactly
--     (floating-point comparison is tolerated via 'forbiddenTolerance' = 1e-9)
--
-- Conditional (if-then) constraints are not handled by this module (they
-- belong to the Custom Design spec; see
-- spec/hanalyze-doe-custom-design-spec.md §2.3 / §9).
--
-- spec: doe-spec v0.2 §2.8 / §3.12.
module Hanalyze.Design.Constraint
  ( ConstraintRel (..)
  , DesignConstraint (..)
  , checkRow
  , checkDesign
  , filterCandidates
  , forbiddenTolerance
  ) where

import qualified Numeric.LinearAlgebra as LA

-- ===========================================================================
-- 型
-- ===========================================================================

-- | [日本語]: 線形制約の関係子。 [English]: The relational operator for a
--   linear constraint.
data ConstraintRel = CLeq | CEq | CGeq
  deriving (Eq, Show)

-- | [日本語]: 設計行列に対する制約。 [English]: A constraint on a design matrix.
data DesignConstraint
  = LinearConstraint     ![Double] !ConstraintRel !Double
    -- ^ [日本語]: @sum_i (coeffs[i] * x[i]) \`rel\` rhs@。 coeffs の長さは row の
    --   次元と一致する必要 ('checkRow' は不一致を即 False として弾く)
    --   [English]: @sum_i (coeffs[i] * x[i]) \`rel\` rhs@. The length of
    --   coeffs must match the row's dimension ('checkRow' rejects a
    --   mismatch immediately as False).
  | ForbiddenCombination ![Double]
    -- ^ [日本語]: row がこの値と (許容誤差 'forbiddenTolerance' で) 一致したら違反。
    --   [English]: Violated when the row matches these values (within the
    --   tolerance 'forbiddenTolerance').
  deriving (Eq, Show)

-- | [日本語]: 'ForbiddenCombination' の浮動小数比較に用いる許容誤差。
--   [English]: The tolerance used for the floating-point comparison in
--   'ForbiddenCombination'.
forbiddenTolerance :: Double
forbiddenTolerance = 1e-9

-- ===========================================================================
-- 公開 API
-- ===========================================================================

-- | [日本語]: 1 row が全制約を満たすか。 制約違反 (= 不可) なら 'False'。
--   [English]: Whether a row satisfies all constraints. 'False' if any
--   constraint is violated (= infeasible).
checkRow :: [DesignConstraint] -> [Double] -> Bool
checkRow cs row = all (rowSatisfies row) cs

-- | [日本語]: 設計行列 (= 各 row が 1 試行) の制約違反 row index を返す。
--   row 数 0 のときは空 list。
--   [English]: Returns the row indices of a design matrix (each row = one
--   trial) that violate a constraint. An empty list when there are 0 rows.
checkDesign :: [DesignConstraint] -> LA.Matrix Double -> [Int]
checkDesign cs m =
  let rows = LA.toLists m
  in [ i | (i, r) <- zip [0 ..] rows, not (checkRow cs r) ]

-- | [日本語]: 候補集合から制約違反 row を除去する helper。 'Hanalyze.Design.Optimal'
--   の入力候補を作る前に挟む想定。 順序は保持。
--   [English]: A helper that removes constraint-violating rows from a
--   candidate set. Intended to be inserted before building the input
--   candidates for 'Hanalyze.Design.Optimal'. Order is preserved.
filterCandidates :: [DesignConstraint] -> [[Double]] -> [[Double]]
filterCandidates cs = filter (checkRow cs)

-- ===========================================================================
-- 内部
-- ===========================================================================

-- | [日本語]: 1 row が単一制約を満たすか判定。
--   [English]: Determines whether a single row satisfies a single constraint.
rowSatisfies :: [Double] -> DesignConstraint -> Bool
rowSatisfies row (LinearConstraint coeffs rel rhs)
  | length coeffs /= length row = False
  | otherwise =
      let lhs = sum (zipWith (*) coeffs row)
      in case rel of
           CLeq -> lhs <= rhs + forbiddenTolerance
           CEq  -> abs (lhs - rhs) <= forbiddenTolerance
           CGeq -> lhs >= rhs - forbiddenTolerance
rowSatisfies row (ForbiddenCombination vals)
  | length vals /= length row = True   -- 次元不一致 = forbidden ではない
  | otherwise =
      not (and (zipWith (\a b -> abs (a - b) <= forbiddenTolerance) row vals))